123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- package clients
- import (
- "context"
- "fmt"
- "strings"
- "git.linuxforward.com/byop/byop-engine/models"
- "github.com/docker/cli/cli/config/configfile"
- clitypes "github.com/docker/cli/cli/config/types"
- "github.com/moby/buildkit/client"
- "github.com/moby/buildkit/client/llb"
- "github.com/moby/buildkit/session"
- "github.com/moby/buildkit/session/auth/authprovider"
- "github.com/sirupsen/logrus"
- "github.com/tonistiigi/fsutil"
- )
- // BuildMachineClient defines the interface for a build machine client.
- type BuildMachineClient interface {
- // BuildImage builds an image based on the job details and build options.
- // It returns the image ID or an equivalent identifier upon success.
- BuildImage(ctx context.Context, job models.BuildJob, dockerfilePath string, contextPath string, imageName string, imageTag string, noCache bool, buildArgs map[string]string) (string, error)
- // PushImage pushes a previously built image to a registry.
- // fullImageURI is the complete URI of the image to push (e.g., myregistry.com/user/image:tag).
- // registryURL is the base URL of the registry (e.g., "docker.io", "myregistry.com") used for auth.
- // username and password are the credentials for the registry.
- PushImage(ctx context.Context, job models.BuildJob, fullImageURI string, registryURL string, username string, password string) error
- // CheckImageExists checks if an image exists in the registry.
- // fullImageURI is the complete URI of the image to check (e.g., myregistry.com/user/image:tag).
- // registryURL is the base URL of the registry (e.g., "docker.io", "myregistry.com") used for auth.
- // username and password are the credentials for the registry.
- CheckImageExists(ctx context.Context, fullImageURI string, registryURL string, username string, password string) (bool, error)
- // Prune can be used to clean up build resources if necessary.
- Prune(ctx context.Context, job models.BuildJob) error // Assuming job might be needed for context, adjust if not.
- // Close releases any resources held by the client.
- Close() error
- }
- // BuildKitClient implements the BuildMachineClient interface using BuildKit.
- type BuildKitClient struct {
- buildkitHost string
- entry *logrus.Entry
- }
- // NewBuildKitClient creates a new BuildKitClient.
- // buildkitHost is the address of the BuildKit daemon (e.g., "tcp://127.0.0.1:1234" or "docker-container://buildkitd")
- func NewBuildKitClient(buildkitHost string) BuildMachineClient {
- return &BuildKitClient{
- buildkitHost: buildkitHost,
- entry: logrus.WithField("component", "BuildKitClient"),
- }
- }
- // getClient ensures a BuildKit client is available.
- // This is a helper to establish a connection on demand or use an existing one.
- // For simplicity in this example, it creates a new client on each major operation,
- // but in a production system, you might want to manage a persistent client.
- func (bkc *BuildKitClient) getClient(ctx context.Context, job models.BuildJob) (*client.Client, error) {
- c, err := client.New(ctx, bkc.buildkitHost, nil)
- if err != nil {
- return nil, fmt.Errorf("job %d: failed to get BuildKit client: %w", job.ID, err)
- }
- return c, nil
- }
- // getClientForPush ensures a BuildKit client is available for push operations.
- // This is a helper to establish a connection on demand or use an existing one.
- func (bkc *BuildKitClient) getClientForPush(ctx context.Context, job models.BuildJob) (*client.Client, error) {
- c, err := client.New(ctx, bkc.buildkitHost, nil)
- if err != nil {
- return nil, fmt.Errorf("job %d: failed to get BuildKit client for push: %w", job.ID, err)
- }
- return c, nil
- }
- // FetchCode is not directly implemented as a separate step in typical BuildKit Dockerfile builds,
- // as the Dockerfile's `COPY` or `ADD` instructions, or git sources, handle this.
- // This method could be used to pre-fetch if needed, or this logic can be integrated into BuildImage.
- // For this implementation, we assume the Dockerfile within the git repo will handle code fetching/access.
- func (bkc *BuildKitClient) FetchCode(job models.BuildJob, sourceURL string, version string, targetDir string) error {
- bkc.entry.Infof("Job %d: FetchCode called (source: %s, version: %s). BuildKit handles this via Dockerfile context or git source.", job.ID, sourceURL, version)
- return nil
- }
- // BuildImage builds a Docker image using BuildKit.
- // dockerfilePath is the path to the Dockerfile *within the git repository context*.
- // contextPath is the sub-directory within the git repository to use as the build context.
- func (bkc *BuildKitClient) BuildImage(ctx context.Context, job models.BuildJob, dockerfilePath string, contextPath string, imageName string, imageTag string, noCache bool, buildArgs map[string]string) (string, error) {
- bkc.entry.Infof("Job %d: Building image %s:%s. SourceURL: %s, BuildContext: %s, Dockerfile: %s, InitialContextPathArg: %s, InitialDockerfileArg: %s", job.ID, imageName, imageTag, job.SourceURL, job.BuildContext, job.Dockerfile, contextPath, dockerfilePath)
- buildkitClient, err := bkc.getClient(ctx, job)
- if err != nil {
- return "", fmt.Errorf("job %d: failed to get BuildKit client: %w", job.ID, err)
- }
- defer buildkitClient.Close()
- localImageName := fmt.Sprintf("%s:%s", imageName, imageTag)
- opts := client.SolveOpt{
- Exports: []client.ExportEntry{
- {
- Type: client.ExporterImage,
- Attrs: map[string]string{
- "name": localImageName,
- },
- },
- },
- LocalDirs: map[string]string{},
- LocalMounts: map[string]fsutil.FS{},
- FrontendAttrs: map[string]string{},
- }
- // Session authentication setup
- dockerCfgFile := configfile.New("") // Path to default Docker config file (~/.docker/config.json)
- // It's okay if this file doesn't exist or is empty; NewDockerAuthProvider handles it.
- // We wrap the ConfigFile in DockerAuthProviderConfig
- defaultAuthConfig := authprovider.DockerAuthProviderConfig{ConfigFile: dockerCfgFile}
- opts.Session = []session.Attachable{authprovider.NewDockerAuthProvider(defaultAuthConfig)}
- // Add specific auth for the target registry if provided in the job (for private base images, etc.)
- if job.RegistryURL != "" && job.RegistryUser != "" && job.RegistryPassword != "" {
- regAuthConfigValue := clitypes.AuthConfig{
- Username: job.RegistryUser,
- Password: job.RegistryPassword,
- }
- normalizedRegURL := job.RegistryURL
- // if job.RegistryURL == "docker.io" || job.RegistryURL == "" { // Docker Hub
- // normalizedRegURL = "https://index.docker.io/v1/"
- // } else if !strings.HasPrefix(job.RegistryURL, "http://") && !strings.HasPrefix(job.RegistryURL, "https://") {
- // normalizedRegURL = "http://" + job.RegistryURL
- // }
- specificAuthCfgFile := configfile.New("") // Create an empty config file object
- specificAuthCfgFile.AuthConfigs[normalizedRegURL] = regAuthConfigValue
- specificAuthConfig := authprovider.DockerAuthProviderConfig{ConfigFile: specificAuthCfgFile}
- opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(specificAuthConfig))
- bkc.entry.Infof("Job %d: Added specific auth for registry %s to build session.", job.ID, normalizedRegURL)
- }
- // LLB support has been removed. Use Dockerfile builds only.
- err = fmt.Errorf("job %d: LLB support has been removed from byop-engine. Please use Dockerfile-based builds", job.ID)
- bkc.entry.Error(err)
- return "", err
- }
- // PushImage pushes a Docker image using BuildKit.
- func (bkc *BuildKitClient) PushImage(ctx context.Context, job models.BuildJob, fullImageURI string, registryURL string, username string, password string) error {
- // LLB support has been removed. Use Dockerfile builds only.
- err := fmt.Errorf("job %d: LLB support has been removed from byop-engine. Please use Dockerfile-based builds", job.ID)
- bkc.entry.Error(err)
- return err
- }
- // CheckImageExists checks if an image exists in the registry using Docker manifest API.
- // This is a simplified implementation that uses BuildKit's registry capabilities.
- func (bkc *BuildKitClient) CheckImageExists(ctx context.Context, fullImageURI string, registryURL string, username string, password string) (bool, error) {
- bkc.entry.Infof("Checking if image exists: %s", fullImageURI)
- // For now, we'll implement a simple approach using BuildKit's ability to reference images
- // We try to create a simple LLB definition that references the image and see if it resolves
- buildkitClient, err := bkc.getClient(ctx, models.BuildJob{ID: 0}) // Dummy job for connection
- if err != nil {
- return false, fmt.Errorf("failed to get BuildKit client for image check: %w", err)
- }
- defer buildkitClient.Close()
- // Create a simple LLB definition that references the image
- // This will fail if the image doesn't exist
- imageState := llb.Image(fullImageURI)
- def, err := imageState.Marshal(ctx)
- if err != nil {
- return false, fmt.Errorf("failed to marshal LLB definition for image check: %w", err)
- }
- opts := client.SolveOpt{
- Exports: []client.ExportEntry{}, // No export, just check if image resolves
- LocalDirs: map[string]string{},
- LocalMounts: map[string]fsutil.FS{},
- FrontendAttrs: map[string]string{},
- }
- // Add authentication if provided
- if username != "" && password != "" {
- serverAddress := registryURL
- if registryURL == "docker.io" || registryURL == "" {
- serverAddress = "https://index.docker.io/v1/"
- } else if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") {
- // For local registries (localhost, host.docker.internal), use HTTP
- if strings.Contains(registryURL, "localhost") || strings.Contains(registryURL, "host.docker.internal") {
- serverAddress = "http://" + registryURL
- } else {
- serverAddress = "https://" + registryURL
- }
- }
- cfgInMemory := &configfile.ConfigFile{
- AuthConfigs: make(map[string]clitypes.AuthConfig),
- }
- cfgInMemory.AuthConfigs[serverAddress] = clitypes.AuthConfig{
- Username: username,
- Password: password,
- }
- authProviderConfig := authprovider.DockerAuthProviderConfig{
- ConfigFile: cfgInMemory,
- }
- opts.Session = []session.Attachable{authprovider.NewDockerAuthProvider(authProviderConfig)}
- }
- // Try to solve the definition - this will fail if the image doesn't exist
- ch := make(chan *client.SolveStatus)
- // Drain the status channel in a goroutine
- go func() {
- for range ch {
- // Consume status updates but don't process them
- }
- }()
- _, err = buildkitClient.Solve(ctx, def, opts, ch)
- if err != nil {
- // Check if error indicates image not found
- errStr := err.Error()
- if strings.Contains(errStr, "not found") ||
- strings.Contains(errStr, "does not exist") ||
- strings.Contains(errStr, "manifest unknown") ||
- strings.Contains(errStr, "pull access denied") {
- bkc.entry.Infof("Image %s does not exist: %v", fullImageURI, err)
- return false, nil
- }
- // Other errors are actual failures
- return false, fmt.Errorf("failed to check image existence: %w", err)
- }
- bkc.entry.Infof("Image %s exists in registry", fullImageURI)
- return true, nil
- }
- // Prune is a no-op for BuildKitClient as it does not manage local resources.
- func (bkc *BuildKitClient) Prune(ctx context.Context, job models.BuildJob) error {
- bkc.entry.Infof("Job %d: Prune called. BuildKit does not manage local resources directly.", job.ID)
- // In a real implementation, you might want to call a BuildKit prune operation if needed.
- return nil
- }
- // Close releases any resources held by the BuildKitClient.
- func (bkc *BuildKitClient) Close() error {
- bkc.entry.Info("Closing BuildKitClient resources.")
- // BuildKit client does not hold persistent resources in this implementation.
- // If you had a persistent client, you would close it here.
- return nil
- }
|