package containers import ( "fmt" "io" "os" "os/exec" "strings" "github.com/google/go-containerregistry/pkg/name" "edge-infra.dev/pkg/lib/build/bazel" ) // Bazel flag aliases for user-defined configuration settings consumed by // our container pushing rules. const ( workloadsRepoFlag = "--workloads_repo" thirdPartyRepoFlag = "--thirdparty_repo" ) // Flags specific to rules_docker container_push binaries. const ( insecureFlag = "insecure" // Allow HTTP repositories. ) const ( // used to detect auth issues and provide helpful feedback permissionErr = "Permission \"artifactregistry.repositories.uploadArtifacts\" denied on resource" garFrag = "docker.pkg.dev" // string fragment for identifying GAR URIs gcrFrag = "gcr.io" // string fragment for identifying GCR URIs ) // Push executes the input container_push targets. It returns a slice of // corresponding references for each target pushed. func Push(bzl *bazel.Bazel, targets []string, opts ...Option) ([]name.Digest, error) { options := makeOptions(opts...) refs := make([]name.Digest, len(targets)) for i, t := range targets { // 2>&1, all cmd output goes to Stdout which is replaced by a pipe cmd := bzl.Run(append([]string{t}, options.args()...)...) cmdOutPipe, err := cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("containers.Push: failed to connect pipe to push command: %w", err) } cmd.Stderr = cmd.Stdout // bazel run | tee ..., execute cmd and copy output into pusha's Stderr tee := io.TeeReader(cmdOutPipe, os.Stderr) if err := cmd.Start(); err != nil { return nil, fmt.Errorf("containers.Push: failed to start push command: %w", err) } out, err := io.ReadAll(tee) if err != nil { return nil, fmt.Errorf("containers.Push: failed to read pipe: %w", err) } if err := cmd.Wait(); err != nil { return nil, fmt.Errorf("containers.Push: failed to execute push command '%s': %w%s: %s", strings.Join(cmd.Args, ""), err, pushHelp(options.registry, err), out, ) } // after push finished, parse output refs[i], err = parseRef(string(out)) if err != nil { return nil, fmt.Errorf( "containers.Push: failed to parse pushed reference for %s: %w", t, err, ) } } return refs, nil } // parseRef parses an OCI reference from the output from a container pushing // rule. func parseRef(out string) (name.Digest, error) { // rules_oci prints the ref out in the middle of the string // parse the text line by line and look for a line and find a valid image // see pkg/tools/hack/containers/testdata for example logs for _, line := range strings.Split(strings.TrimSuffix(out, "\n"), "\n") { digest, err := name.NewDigest(line) if err == nil { return digest, nil } } // if we didnt find anything with the above its probably rules_docker // Get last space separated element of output, to drop noise from rules_docker // ❯ just push test/rosa/... // ... // INFO: Running command line: bazel-bin/test/rosa/container_push // 2024/02/01 15:2... - us-east1-docker.pkg.dev/ret-edge-pltf-infra/workloads/lumperctl@sha256:d98d67 tokens := strings.Split(strings.TrimSpace(strings.ReplaceAll(out, "\r\n", " ")), " ") return name.NewDigest(tokens[len(tokens)-1]) } // QueryPushes queries Bazel for all container_push targets within the set of // targets in the expression. func QueryPushes(expr ...string) ([]string, error) { pushes, err := bazel.QueryFromFile(fmt.Sprintf( "kind('container_push2', '%s')", strings.Join(expr, " "), )) if err != nil { return nil, fmt.Errorf("containers.QueryPushes: failed to query for container_push targets. error: %w", err) } return pushes, nil } // Build builds one or more push targets without actually pushing. This produces // a digest that can be used for automation / processing without a direct // dependency on a remote OCI registry. func Build(bzl *bazel.Bazel, targets []string, opts ...Option) error { options := makeOptions(opts...) if err := bzl.Build(append(targets, options.args()...)...).Run(); err != nil { return fmt.Errorf("containers.Build: bazel build failed. error: %w", err) } return nil } func pushHelp(registry string, err error) string { switch { // cant/dont provide guidance for non-GCP registries case !strings.Contains(registry, garFrag) && !strings.Contains(registry, gcrFrag): return "" case strings.Contains(err.Error(), permissionErr): helpText := "" if strings.Contains(err.Error(), permissionErr) { command := "gcloud" args := []string{"auth", "list", "--filter=status:ACTIVE", "--format=value(account)"} out, err := exec.Command(command, args...).CombinedOutput() var acct string if err != nil { acct = fmt.Sprintf("unknown (error executing gcloud auth: %s)", err.Error()) } else { acct = strings.TrimSpace(string(out)) } cmd := fmt.Sprintf("gcloud auth configure-docker %s", registry) helpText = fmt.Sprintf("\n\ncurrent Google Cloud account is: %vn\nif you believe this account should have access, you may need to run:\n $ %s\n", acct, cmd) } return helpText default: return "" } }