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 = "https://" + 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://") { 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 }