buildkit.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package clients
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "git.linuxforward.com/byop/byop-engine/models"
  7. "github.com/docker/cli/cli/config/configfile"
  8. clitypes "github.com/docker/cli/cli/config/types"
  9. "github.com/moby/buildkit/client"
  10. "github.com/moby/buildkit/client/llb"
  11. "github.com/moby/buildkit/session"
  12. "github.com/moby/buildkit/session/auth/authprovider"
  13. "github.com/sirupsen/logrus"
  14. "github.com/tonistiigi/fsutil"
  15. )
  16. // BuildMachineClient defines the interface for a build machine client.
  17. type BuildMachineClient interface {
  18. // BuildImage builds an image based on the job details and build options.
  19. // It returns the image ID or an equivalent identifier upon success.
  20. BuildImage(ctx context.Context, job models.BuildJob, dockerfilePath string, contextPath string, imageName string, imageTag string, noCache bool, buildArgs map[string]string) (string, error)
  21. // PushImage pushes a previously built image to a registry.
  22. // fullImageURI is the complete URI of the image to push (e.g., myregistry.com/user/image:tag).
  23. // registryURL is the base URL of the registry (e.g., "docker.io", "myregistry.com") used for auth.
  24. // username and password are the credentials for the registry.
  25. PushImage(ctx context.Context, job models.BuildJob, fullImageURI string, registryURL string, username string, password string) error
  26. // CheckImageExists checks if an image exists in the registry.
  27. // fullImageURI is the complete URI of the image to check (e.g., myregistry.com/user/image:tag).
  28. // registryURL is the base URL of the registry (e.g., "docker.io", "myregistry.com") used for auth.
  29. // username and password are the credentials for the registry.
  30. CheckImageExists(ctx context.Context, fullImageURI string, registryURL string, username string, password string) (bool, error)
  31. // Prune can be used to clean up build resources if necessary.
  32. Prune(ctx context.Context, job models.BuildJob) error // Assuming job might be needed for context, adjust if not.
  33. // Close releases any resources held by the client.
  34. Close() error
  35. }
  36. // BuildKitClient implements the BuildMachineClient interface using BuildKit.
  37. type BuildKitClient struct {
  38. buildkitHost string
  39. entry *logrus.Entry
  40. }
  41. // NewBuildKitClient creates a new BuildKitClient.
  42. // buildkitHost is the address of the BuildKit daemon (e.g., "tcp://127.0.0.1:1234" or "docker-container://buildkitd")
  43. func NewBuildKitClient(buildkitHost string) BuildMachineClient {
  44. return &BuildKitClient{
  45. buildkitHost: buildkitHost,
  46. entry: logrus.WithField("component", "BuildKitClient"),
  47. }
  48. }
  49. // getClient ensures a BuildKit client is available.
  50. // This is a helper to establish a connection on demand or use an existing one.
  51. // For simplicity in this example, it creates a new client on each major operation,
  52. // but in a production system, you might want to manage a persistent client.
  53. func (bkc *BuildKitClient) getClient(ctx context.Context, job models.BuildJob) (*client.Client, error) {
  54. c, err := client.New(ctx, bkc.buildkitHost, nil)
  55. if err != nil {
  56. return nil, fmt.Errorf("job %d: failed to get BuildKit client: %w", job.ID, err)
  57. }
  58. return c, nil
  59. }
  60. // getClientForPush ensures a BuildKit client is available for push operations.
  61. // This is a helper to establish a connection on demand or use an existing one.
  62. func (bkc *BuildKitClient) getClientForPush(ctx context.Context, job models.BuildJob) (*client.Client, error) {
  63. c, err := client.New(ctx, bkc.buildkitHost, nil)
  64. if err != nil {
  65. return nil, fmt.Errorf("job %d: failed to get BuildKit client for push: %w", job.ID, err)
  66. }
  67. return c, nil
  68. }
  69. // FetchCode is not directly implemented as a separate step in typical BuildKit Dockerfile builds,
  70. // as the Dockerfile's `COPY` or `ADD` instructions, or git sources, handle this.
  71. // This method could be used to pre-fetch if needed, or this logic can be integrated into BuildImage.
  72. // For this implementation, we assume the Dockerfile within the git repo will handle code fetching/access.
  73. func (bkc *BuildKitClient) FetchCode(job models.BuildJob, sourceURL string, version string, targetDir string) error {
  74. bkc.entry.Infof("Job %d: FetchCode called (source: %s, version: %s). BuildKit handles this via Dockerfile context or git source.", job.ID, sourceURL, version)
  75. return nil
  76. }
  77. // BuildImage builds a Docker image using BuildKit.
  78. // dockerfilePath is the path to the Dockerfile *within the git repository context*.
  79. // contextPath is the sub-directory within the git repository to use as the build context.
  80. 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) {
  81. 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)
  82. buildkitClient, err := bkc.getClient(ctx, job)
  83. if err != nil {
  84. return "", fmt.Errorf("job %d: failed to get BuildKit client: %w", job.ID, err)
  85. }
  86. defer buildkitClient.Close()
  87. localImageName := fmt.Sprintf("%s:%s", imageName, imageTag)
  88. opts := client.SolveOpt{
  89. Exports: []client.ExportEntry{
  90. {
  91. Type: client.ExporterImage,
  92. Attrs: map[string]string{
  93. "name": localImageName,
  94. },
  95. },
  96. },
  97. LocalDirs: map[string]string{},
  98. LocalMounts: map[string]fsutil.FS{},
  99. FrontendAttrs: map[string]string{},
  100. }
  101. // Session authentication setup
  102. dockerCfgFile := configfile.New("") // Path to default Docker config file (~/.docker/config.json)
  103. // It's okay if this file doesn't exist or is empty; NewDockerAuthProvider handles it.
  104. // We wrap the ConfigFile in DockerAuthProviderConfig
  105. defaultAuthConfig := authprovider.DockerAuthProviderConfig{ConfigFile: dockerCfgFile}
  106. opts.Session = []session.Attachable{authprovider.NewDockerAuthProvider(defaultAuthConfig)}
  107. // Add specific auth for the target registry if provided in the job (for private base images, etc.)
  108. if job.RegistryURL != "" && job.RegistryUser != "" && job.RegistryPassword != "" {
  109. regAuthConfigValue := clitypes.AuthConfig{
  110. Username: job.RegistryUser,
  111. Password: job.RegistryPassword,
  112. }
  113. normalizedRegURL := job.RegistryURL
  114. if job.RegistryURL == "docker.io" || job.RegistryURL == "" { // Docker Hub
  115. normalizedRegURL = "https://index.docker.io/v1/"
  116. } else if !strings.HasPrefix(job.RegistryURL, "http://") && !strings.HasPrefix(job.RegistryURL, "https://") {
  117. normalizedRegURL = "https://" + job.RegistryURL
  118. }
  119. specificAuthCfgFile := configfile.New("") // Create an empty config file object
  120. specificAuthCfgFile.AuthConfigs[normalizedRegURL] = regAuthConfigValue
  121. specificAuthConfig := authprovider.DockerAuthProviderConfig{ConfigFile: specificAuthCfgFile}
  122. opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(specificAuthConfig))
  123. bkc.entry.Infof("Job %d: Added specific auth for registry %s to build session.", job.ID, normalizedRegURL)
  124. }
  125. // LLB support has been removed. Use Dockerfile builds only.
  126. err = fmt.Errorf("job %d: LLB support has been removed from byop-engine. Please use Dockerfile-based builds", job.ID)
  127. bkc.entry.Error(err)
  128. return "", err
  129. }
  130. // PushImage pushes a Docker image using BuildKit.
  131. func (bkc *BuildKitClient) PushImage(ctx context.Context, job models.BuildJob, fullImageURI string, registryURL string, username string, password string) error {
  132. // LLB support has been removed. Use Dockerfile builds only.
  133. err := fmt.Errorf("job %d: LLB support has been removed from byop-engine. Please use Dockerfile-based builds", job.ID)
  134. bkc.entry.Error(err)
  135. return err
  136. }
  137. // CheckImageExists checks if an image exists in the registry using Docker manifest API.
  138. // This is a simplified implementation that uses BuildKit's registry capabilities.
  139. func (bkc *BuildKitClient) CheckImageExists(ctx context.Context, fullImageURI string, registryURL string, username string, password string) (bool, error) {
  140. bkc.entry.Infof("Checking if image exists: %s", fullImageURI)
  141. // For now, we'll implement a simple approach using BuildKit's ability to reference images
  142. // We try to create a simple LLB definition that references the image and see if it resolves
  143. buildkitClient, err := bkc.getClient(ctx, models.BuildJob{ID: 0}) // Dummy job for connection
  144. if err != nil {
  145. return false, fmt.Errorf("failed to get BuildKit client for image check: %w", err)
  146. }
  147. defer buildkitClient.Close()
  148. // Create a simple LLB definition that references the image
  149. // This will fail if the image doesn't exist
  150. imageState := llb.Image(fullImageURI)
  151. def, err := imageState.Marshal(ctx)
  152. if err != nil {
  153. return false, fmt.Errorf("failed to marshal LLB definition for image check: %w", err)
  154. }
  155. opts := client.SolveOpt{
  156. Exports: []client.ExportEntry{}, // No export, just check if image resolves
  157. LocalDirs: map[string]string{},
  158. LocalMounts: map[string]fsutil.FS{},
  159. FrontendAttrs: map[string]string{},
  160. }
  161. // Add authentication if provided
  162. if username != "" && password != "" {
  163. serverAddress := registryURL
  164. if registryURL == "docker.io" || registryURL == "" {
  165. serverAddress = "https://index.docker.io/v1/"
  166. } else if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") {
  167. serverAddress = "https://" + registryURL
  168. }
  169. cfgInMemory := &configfile.ConfigFile{
  170. AuthConfigs: make(map[string]clitypes.AuthConfig),
  171. }
  172. cfgInMemory.AuthConfigs[serverAddress] = clitypes.AuthConfig{
  173. Username: username,
  174. Password: password,
  175. }
  176. authProviderConfig := authprovider.DockerAuthProviderConfig{
  177. ConfigFile: cfgInMemory,
  178. }
  179. opts.Session = []session.Attachable{authprovider.NewDockerAuthProvider(authProviderConfig)}
  180. }
  181. // Try to solve the definition - this will fail if the image doesn't exist
  182. ch := make(chan *client.SolveStatus)
  183. // Drain the status channel in a goroutine
  184. go func() {
  185. for range ch {
  186. // Consume status updates but don't process them
  187. }
  188. }()
  189. _, err = buildkitClient.Solve(ctx, def, opts, ch)
  190. if err != nil {
  191. // Check if error indicates image not found
  192. errStr := err.Error()
  193. if strings.Contains(errStr, "not found") ||
  194. strings.Contains(errStr, "does not exist") ||
  195. strings.Contains(errStr, "manifest unknown") ||
  196. strings.Contains(errStr, "pull access denied") {
  197. bkc.entry.Infof("Image %s does not exist: %v", fullImageURI, err)
  198. return false, nil
  199. }
  200. // Other errors are actual failures
  201. return false, fmt.Errorf("failed to check image existence: %w", err)
  202. }
  203. bkc.entry.Infof("Image %s exists in registry", fullImageURI)
  204. return true, nil
  205. }
  206. // Prune is a no-op for BuildKitClient as it does not manage local resources.
  207. func (bkc *BuildKitClient) Prune(ctx context.Context, job models.BuildJob) error {
  208. bkc.entry.Infof("Job %d: Prune called. BuildKit does not manage local resources directly.", job.ID)
  209. // In a real implementation, you might want to call a BuildKit prune operation if needed.
  210. return nil
  211. }
  212. // Close releases any resources held by the BuildKitClient.
  213. func (bkc *BuildKitClient) Close() error {
  214. bkc.entry.Info("Closing BuildKitClient resources.")
  215. // BuildKit client does not hold persistent resources in this implementation.
  216. // If you had a persistent client, you would close it here.
  217. return nil
  218. }