package main import ( "encoding/base64" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "github.com/joho/godotenv" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/urfave/cli" kaniko "github.com/drone/drone-kaniko" "github.com/drone/drone-kaniko/pkg/artifact" "github.com/drone/drone-kaniko/pkg/docker" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" "github.com/drone/drone-kaniko/pkg/utils" ) const ( dockerConfigPath string = "/kaniko/.docker" // GAR JSON key file path garKeyPath string = "/kaniko/config.json" garEnvVariable string = "GOOGLE_APPLICATION_CREDENTIALS" defaultDigestFile string = "/kaniko/digest-file" ) var ( version = "unknown" ) func main() { // Load env-file if it exists first if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { if err := godotenv.Load(env); err != nil { logrus.Fatal(err) } } app := cli.NewApp() app.Name = "kaniko gar plugin" app.Usage = "kaniko gar plugin" app.Action = run app.Version = version app.Flags = []cli.Flag{ cli.StringFlag{ Name: "dockerfile", Usage: "build dockerfile", Value: "Dockerfile", EnvVar: "PLUGIN_DOCKERFILE", }, cli.StringFlag{ Name: "context", Usage: "build context", Value: ".", EnvVar: "PLUGIN_CONTEXT", }, cli.StringFlag{ Name: "drone-commit-ref", Usage: "git commit ref passed by Drone", EnvVar: "DRONE_COMMIT_REF", }, cli.StringFlag{ Name: "drone-repo-branch", Usage: "git repository default branch passed by Drone", EnvVar: "DRONE_REPO_BRANCH", }, cli.StringSliceFlag{ Name: "tags", Usage: "build tags", Value: &cli.StringSlice{"latest"}, EnvVar: "PLUGIN_TAGS", FilePath: ".tags", }, cli.BoolFlag{ Name: "expand-tag", Usage: "enable for semver tagging", EnvVar: "PLUGIN_EXPAND_TAG", }, cli.BoolFlag{ Name: "auto-tag", Usage: "enable auto generation of build tags", EnvVar: "PLUGIN_AUTO_TAG", }, cli.StringFlag{ Name: "auto-tag-suffix", Usage: "the suffix of auto build tags", EnvVar: "PLUGIN_AUTO_TAG_SUFFIX", }, cli.StringSliceFlag{ Name: "args", Usage: "build args", EnvVar: "PLUGIN_BUILD_ARGS", }, cli.GenericFlag{ Name: "args-new", Usage: "build args new", EnvVar: "PLUGIN_BUILD_ARGS_NEW", Value: new(utils.CustomStringSliceFlag), }, cli.BoolFlag{ Name: "plugin-multiple-build-agrs", Usage: "plugin multiple build agrs", EnvVar: "PLUGIN_MULTIPLE_BUILD_ARGS", }, cli.StringFlag{ Name: "target", Usage: "build target", EnvVar: "PLUGIN_TARGET", }, cli.StringFlag{ Name: "repo", Usage: "gar repository", EnvVar: "PLUGIN_REPO", }, cli.StringSliceFlag{ Name: "custom-labels", Usage: "additional k=v labels", EnvVar: "PLUGIN_CUSTOM_LABELS", }, cli.StringFlag{ Name: "registry", Usage: "gar registry", EnvVar: "PLUGIN_REGISTRY", }, cli.StringFlag{ Name: "base-image-username", Usage: "Docker username for base image registry", EnvVar: "PLUGIN_DOCKER_USERNAME,PLUGIN_BASE_IMAGE_USERNAME,DOCKER_USERNAME", }, cli.StringFlag{ Name: "base-image-password", Usage: "Docker password for base image registry", EnvVar: "PLUGIN_DOCKER_PASSWORD,PLUGIN_BASE_IMAGE_PASSWORD,DOCKER_PASSWORD", }, cli.StringFlag{ Name: "base-image-registry", Usage: "Docker registry for base image registry", EnvVar: "PLUGIN_DOCKER_REGISTRY,PLUGIN_BASE_IMAGE_REGISTRY,DOCKER_REGISTRY", }, cli.StringSliceFlag{ Name: "registry-mirrors", Usage: "docker registry mirrors", EnvVar: "PLUGIN_REGISTRY_MIRRORS", }, cli.StringFlag{ Name: "json-key", Usage: "docker username", EnvVar: "PLUGIN_JSON_KEY", }, cli.StringFlag{ Name: "snapshot-mode", Usage: "Specify one of full, redo or time as snapshot mode", EnvVar: "PLUGIN_SNAPSHOT_MODE", }, cli.BoolFlag{ Name: "enable-cache", Usage: "Set this flag to opt into caching with kaniko", EnvVar: "PLUGIN_ENABLE_CACHE", }, cli.StringFlag{ Name: "cache-repo", Usage: "Remote repository that will be used to store cached layers. Cache repo should be present in specified registry. enable-cache needs to be set to use this flag", EnvVar: "PLUGIN_CACHE_REPO", }, cli.IntFlag{ Name: "cache-ttl", Usage: "Cache timeout in hours. Defaults to two weeks.", EnvVar: "PLUGIN_CACHE_TTL", }, cli.StringFlag{ Name: "artifact-file", Usage: "Artifact file location that will be generated by the plugin. This file will include information of docker images that are uploaded by the plugin.", EnvVar: "PLUGIN_ARTIFACT_FILE", }, cli.BoolFlag{ Name: "no-push", Usage: "Set this flag if you only want to build the image, without pushing to a registry", EnvVar: "PLUGIN_NO_PUSH", }, cli.BoolFlag{ Name: "push-only", Usage: "Set this flag if you only want to push a pre-built image from a tarball", EnvVar: "PLUGIN_PUSH_ONLY", }, cli.StringFlag{ Name: "source-tar-path", Usage: "Path to the local tarball to be pushed when push-only is set", EnvVar: "PLUGIN_SOURCE_TAR_PATH", }, cli.StringFlag{ Name: "tar-path", Usage: "Set this flag to save the image as a tarball at path", EnvVar: "PLUGIN_TAR_PATH,PLUGIN_DESTINATION_TAR_PATH", }, cli.StringFlag{ Name: "verbosity", Usage: "Set this flag as --verbosity= to set the logging level for kaniko. Defaults to info.", EnvVar: "PLUGIN_VERBOSITY", }, cli.StringFlag{ Name: "platform", Usage: "Allows to build with another default platform than the host, similarly to docker build --platform", EnvVar: "PLUGIN_PLATFORM,PLUGIN_CUSTOM_PLATFORM", }, cli.BoolFlag{ Name: "skip-unused-stages", Usage: "build only used stages", EnvVar: "PLUGIN_SKIP_UNUSED_STAGES", }, cli.StringFlag{ Name: "cache-dir", Usage: "Set this flag to specify a local directory cache for base images", EnvVar: "PLUGIN_CACHE_DIR", }, cli.BoolFlag{ Name: "cache-copy-layers", Usage: "Enable or disable copying layers from the cache.", EnvVar: "PLUGIN_CACHE_COPY_LAYERS", }, cli.BoolFlag{ Name: "cache-run-layers", Usage: "Enable or disable running layers from the cache.", EnvVar: "PLUGIN_CACHE_RUN_LAYERS", }, cli.BoolFlag{ Name: "cleanup", Usage: "Enable or disable cleanup of temporary files.", EnvVar: "PLUGIN_CLEANUP", }, cli.BoolFlag{ Name: "compressed-caching", Usage: "Enable or disable compressed caching.", EnvVar: "PLUGIN_COMPRESSED_CACHING", }, cli.StringFlag{ Name: "context-sub-path", Usage: "Sub-path within the context to build.", EnvVar: "PLUGIN_CONTEXT_SUB_PATH", }, cli.BoolFlag{ Name: "force", Usage: "Force building the image even if it already exists.", EnvVar: "PLUGIN_FORCE", }, cli.StringFlag{ Name: "image-name-with-digest-file", Usage: "Write image name with digest to a file.", EnvVar: "PLUGIN_IMAGE_NAME_WITH_DIGEST_FILE", }, cli.StringFlag{ Name: "image-name-tag-with-digest-file", Usage: "Write image name with tag and digest to a file.", EnvVar: "PLUGIN_IMAGE_NAME_TAG_WITH_DIGEST_FILE", }, cli.BoolFlag{ Name: "insecure", Usage: "Allow connecting to registries without TLS.", EnvVar: "PLUGIN_INSECURE", }, cli.BoolFlag{ Name: "insecure-pull", Usage: "Allow insecure pulls from the registry.", EnvVar: "PLUGIN_INSECURE_PULL", }, cli.StringFlag{ Name: "insecure-registry", Usage: "Use plain HTTP for registry communication.", EnvVar: "PLUGIN_INSECURE_REGISTRY", }, cli.StringFlag{ Name: "log-format", Usage: "Set the log format for build output.", EnvVar: "PLUGIN_LOG_FORMAT", }, cli.BoolFlag{ Name: "log-timestamp", Usage: "Show timestamps in build output.", EnvVar: "PLUGIN_LOG_TIMESTAMP", }, cli.StringFlag{ Name: "oci-layout-path", Usage: "Directory to store OCI layout.", EnvVar: "PLUGIN_OCI_LAYOUT_PATH", }, cli.IntFlag{ Name: "push-retry", Usage: "Number of times to retry pushing an image.", EnvVar: "PLUGIN_PUSH_RETRY", }, cli.StringFlag{ Name: "registry-certificate", Usage: "Path to a file containing a registry certificate.", EnvVar: "PLUGIN_REGISTRY_CERTIFICATE", }, cli.StringFlag{ Name: "registry-client-cert", Usage: "Path to a file containing a registry client certificate.", EnvVar: "PLUGIN_REGISTRY_CLIENT_CERT", }, cli.BoolFlag{ Name: "skip-default-registry-fallback", Usage: "Skip Docker Hub and default registry fallback.", EnvVar: "PLUGIN_SKIP_DEFAULT_REGISTRY_FALLBACK", }, cli.BoolFlag{ Name: "reproducible", Usage: "Create a reproducible image.", EnvVar: "PLUGIN_REPRODUCIBLE", }, cli.BoolFlag{ Name: "single-snapshot", Usage: "Only create a single snapshot of the image.", EnvVar: "PLUGIN_SINGLE_SNAPSHOT", }, cli.BoolFlag{ Name: "skip-push-permission-check", Usage: "Skip permission check when pushing.", EnvVar: "PLUGIN_SKIP_PUSH_PERMISSION_CHECK", }, cli.BoolFlag{ Name: "skip-tls-verify-pull", Usage: "Skip TLS verification when pulling.", EnvVar: "PLUGIN_SKIP_TLS_VERIFY_PULL", }, cli.BoolFlag{ Name: "skip-tls-verify-registry", Usage: "Skip TLS verification when connecting to a registry.", EnvVar: "PLUGIN_SKIP_TLS_VERIFY_REGISTRY", }, cli.BoolFlag{ Name: "use-new-run", Usage: "Skip TLS verification when connecting to a registry.", EnvVar: "PLUGIN_USE_NEW_RUN", }, cli.BoolFlag{ Name: "ignore-var-run", Usage: "Ignore the /var/run directory during build.", EnvVar: "PLUGIN_IGNORE_VAR_RUN", }, cli.StringFlag{ Name: "ignore-path", Usage: "Path to ignore during the build.", EnvVar: "PLUGIN_IGNORE_PATH", }, cli.IntFlag{ Name: "image-fs-extract-retry", Usage: "Number of retries for extracting filesystem layers.", EnvVar: "PLUGIN_IMAGE_FS_EXTRACT_RETRY", }, cli.IntFlag{ Name: "image-download-retry", Usage: "Number of retries for downloading base images.", EnvVar: "PLUGIN_IMAGE_DOWNLOAD_RETRY", }, } if err := app.Run(os.Args); err != nil { logrus.Fatal(err) } } func run(c *cli.Context) error { // Check if this is a push-only operation if c.Bool("push-only") { return handlePushOnly(c) } noPush := c.Bool("no-push") jsonKey := c.String("json-key") // JSON key may not be set in the following cases: // 1. Image does not need to be pushed to GAR. // 2. Workload identity is set on GKE in which pod will inherit the credentials via service account. if jsonKey != "" { if err := setupGARAuth(jsonKey); err != nil { return err } // setup docker config only when base image registry is specified if c.String("base-image-registry") != "" { if err := setDockerAuth( c.String("base-image-username"), c.String("base-image-password"), c.String("base-image-registry"), ); err != nil { return errors.Wrap(err, "failed to create docker config") } } else { fmt.Println("\033[33mTo ensure consistent and reliable pipeline execution, we recommend setting up a Base Image Connector.\033[0m\n" + "\033[33mWhile optional at this time, configuring it helps prevent failures caused by Docker Hub's rate limits.\033[0m") } } plugin := kaniko.Plugin{ Build: kaniko.Build{ DroneCommitRef: c.String("drone-commit-ref"), DroneRepoBranch: c.String("drone-repo-branch"), Dockerfile: c.String("dockerfile"), Context: c.String("context"), Tags: c.StringSlice("tags"), AutoTag: c.Bool("auto-tag"), AutoTagSuffix: c.String("auto-tag-suffix"), ExpandTag: c.Bool("expand-tag"), Args: c.StringSlice("args"), ArgsNew: c.Generic("args-new").(*utils.CustomStringSliceFlag).GetValue(), IsMultipleBuildArgs: c.Bool("plugin-multiple-build-agrs"), Target: c.String("target"), Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), Mirrors: c.StringSlice("registry-mirrors"), Labels: c.StringSlice("custom-labels"), SnapshotMode: c.String("snapshot-mode"), EnableCache: c.Bool("enable-cache"), CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), CacheTTL: c.Int("cache-ttl"), DigestFile: defaultDigestFile, NoPush: noPush, PushOnly: c.Bool("push-only"), SourceTarPath: c.String("source-tar-path"), TarPath: c.String("tar-path"), Verbosity: c.String("verbosity"), CustomPlatform: c.String("platform"), SkipUnusedStages: c.Bool("skip-unused-stages"), CacheDir: c.String("cache-dir"), CacheCopyLayers: c.Bool("cache-copy-layers"), CacheRunLayers: c.Bool("cache-run-layers"), Cleanup: c.Bool("cleanup"), ContextSubPath: c.String("context-sub-path"), Force: c.Bool("force"), ImageNameWithDigestFile: c.String("image-name-with-digest-file"), ImageNameTagWithDigestFile: c.String("image-name-tag-with-digest-file"), Insecure: c.Bool("insecure"), InsecurePull: c.Bool("insecure-pull"), InsecureRegistry: c.String("insecure-registry"), Label: c.String("label"), LogFormat: c.String("log-format"), LogTimestamp: c.Bool("log-timestamp"), OCILayoutPath: c.String("oci-layout-path"), PushRetry: c.Int("push-retry"), RegistryCertificate: c.String("registry-certificate"), RegistryClientCert: c.String("registry-client-cert"), SkipDefaultRegistryFallback: c.Bool("skip-default-registry-fallback"), Reproducible: c.Bool("reproducible"), SingleSnapshot: c.Bool("single-snapshot"), SkipTLSVerify: c.Bool("skip-tls-verify"), SkipPushPermissionCheck: c.Bool("skip-push-permission-check"), SkipTLSVerifyPull: c.Bool("skip-tls-verify-pull"), SkipTLSVerifyRegistry: c.Bool("skip-tls-verify-registry"), UseNewRun: c.Bool("use-new-run"), IgnorePath: c.String("ignore-path"), IgnorePaths: c.StringSlice("ignore-paths"), ImageFSExtractRetry: c.Int("image-fs-extract-retry"), ImageDownloadRetry: c.Int("image-download-retry"), }, Artifact: kaniko.Artifact{ Tags: c.StringSlice("tags"), Repo: c.String("repo"), Registry: c.String("registry"), ArtifactFile: c.String("artifact-file"), RegistryType: artifact.GAR, }, } if c.IsSet("compressed-caching") { flag := c.Bool("compressed-caching") plugin.Build.CompressedCaching = &flag } if c.IsSet("ignore-var-run") { flag := c.Bool("ignore-var-run") plugin.Build.IgnoreVarRun = &flag } return plugin.Exec() } func setDockerAuth(dockerUsername, dockerPassword, dockerRegistry string) error { dockerConfig := docker.NewConfig() dockerRegistryCreds := docker.RegistryCredentials{ Registry: dockerRegistry, Username: dockerUsername, Password: dockerPassword, } credentials := []docker.RegistryCredentials{dockerRegistryCreds} return dockerConfig.CreateDockerConfig(credentials, dockerConfigPath) } func setupGARAuth(jsonKey string) error { err := ioutil.WriteFile(garKeyPath, []byte(jsonKey), 0644) if err != nil { return errors.Wrap(err, "failed to write GAR JSON key") } err = os.Setenv(garEnvVariable, garKeyPath) if err != nil { return errors.Wrap(err, fmt.Sprintf("failed to set %s environment variable", garEnvVariable)) } return nil } func handlePushOnly(c *cli.Context) error { // Validate inputs for push-only operation sourceTarPath := c.String("source-tar-path") if sourceTarPath == "" { return fmt.Errorf("source_tar_path is required when push_only is set") } if _, err := os.Stat(sourceTarPath); os.IsNotExist(err) { return fmt.Errorf("image tarball does not exist at path: %s", sourceTarPath) } repo := c.String("repo") registry := c.String("registry") if repo == "" || registry == "" { return fmt.Errorf("repository and registry must be specified for push-only operation") } // Authentication options for crane var opts []crane.Option // Setup GAR authentication jsonKey := c.String("json-key") if jsonKey != "" { if err := setupGARAuth(jsonKey); err != nil { return err } logrus.Info("Setting up authentication for GAR") // Create Docker config directory if it doesn't exist dockerConfigDir := "/kaniko/.docker" if err := os.MkdirAll(dockerConfigDir, 0755); err != nil { return fmt.Errorf("failed to create Docker config directory: %v", err) } // Generate a Docker config with GAR auth type DockerAuth struct { Username string `json:"username"` Password string `json:"password"` Auth string `json:"auth"` } type DockerConfig struct { Auths map[string]DockerAuth `json:"auths"` } // Create proper Auth field (base64 encoded username:password) username := "_json_key" authString := base64.StdEncoding.EncodeToString([]byte(username + ":" + jsonKey)) // Use _json_key as username and the key content as password for GAR config := DockerConfig{ Auths: map[string]DockerAuth{ registry: { Username: username, Password: jsonKey, Auth: authString, }, }, } // Write the Docker config configBytes, err := json.Marshal(config) if err != nil { return fmt.Errorf("failed to marshal Docker config: %v", err) } dockerConfigPath := filepath.Join(dockerConfigDir, "config.json") if err := ioutil.WriteFile(dockerConfigPath, configBytes, 0644); err != nil { return fmt.Errorf("failed to write Docker config: %v", err) } // Explicitly set DOCKER_CONFIG environment variable to ensure crane finds the config if err := os.Setenv("DOCKER_CONFIG", dockerConfigDir); err != nil { return fmt.Errorf("failed to set DOCKER_CONFIG environment variable: %v", err) } // Set up crane to use basic auth with docker config opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain)) } else { logrus.Warn("No JSON key provided, authentication may fail if not running with workload identity") } // Load the image from the tarball logrus.Infof("Loading image from tarball: %s", sourceTarPath) img, err := crane.Load(sourceTarPath) if err != nil { return fmt.Errorf("failed to load image from tarball: %v", err) } // Push for each tag tags := c.StringSlice("tags") if len(tags) == 0 { tags = []string{"latest"} } for _, tag := range tags { dest := fmt.Sprintf("%s/%s:%s", registry, repo, tag) logrus.Infof("Pushing image to: %s", dest) if err := crane.Push(img, dest, opts...); err != nil { return fmt.Errorf("failed to push image to %s: %v", dest, err) } logrus.Infof("Successfully pushed image to %s", dest) } return nil }