...

Source file src/edge-infra.dev/pkg/tools/hack/containers/push.go

Documentation: edge-infra.dev/pkg/tools/hack/containers

     1  package containers
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	"github.com/google/go-containerregistry/pkg/name"
    11  
    12  	"edge-infra.dev/pkg/lib/build/bazel"
    13  )
    14  
    15  // Bazel flag aliases for user-defined configuration settings consumed by
    16  // our container pushing rules.
    17  const (
    18  	workloadsRepoFlag  = "--workloads_repo"
    19  	thirdPartyRepoFlag = "--thirdparty_repo"
    20  )
    21  
    22  // Flags specific to rules_docker container_push binaries.
    23  const (
    24  	insecureFlag = "insecure" // Allow HTTP repositories.
    25  )
    26  
    27  const (
    28  	// used to detect auth issues and provide helpful feedback
    29  	permissionErr = "Permission \"artifactregistry.repositories.uploadArtifacts\" denied on resource"
    30  	garFrag       = "docker.pkg.dev" // string fragment for identifying GAR URIs
    31  	gcrFrag       = "gcr.io"         // string fragment for identifying GCR URIs
    32  )
    33  
    34  // Push executes the input container_push targets. It returns a slice of
    35  // corresponding references for each target pushed.
    36  func Push(bzl *bazel.Bazel, targets []string, opts ...Option) ([]name.Digest, error) {
    37  	options := makeOptions(opts...)
    38  
    39  	refs := make([]name.Digest, len(targets))
    40  	for i, t := range targets {
    41  		// 2>&1, all cmd output goes to Stdout which is replaced by a pipe
    42  		cmd := bzl.Run(append([]string{t}, options.args()...)...)
    43  		cmdOutPipe, err := cmd.StdoutPipe()
    44  		if err != nil {
    45  			return nil, fmt.Errorf("containers.Push: failed to connect pipe to push command: %w", err)
    46  		}
    47  		cmd.Stderr = cmd.Stdout
    48  
    49  		// bazel run | tee ..., execute cmd and copy output into pusha's Stderr
    50  		tee := io.TeeReader(cmdOutPipe, os.Stderr)
    51  		if err := cmd.Start(); err != nil {
    52  			return nil, fmt.Errorf("containers.Push: failed to start push command: %w", err)
    53  		}
    54  		out, err := io.ReadAll(tee)
    55  		if err != nil {
    56  			return nil, fmt.Errorf("containers.Push: failed to read pipe: %w", err)
    57  		}
    58  		if err := cmd.Wait(); err != nil {
    59  			return nil, fmt.Errorf("containers.Push: failed to execute push command '%s': %w%s: %s",
    60  				strings.Join(cmd.Args, ""), err, pushHelp(options.registry, err), out,
    61  			)
    62  		}
    63  
    64  		// after push finished, parse output
    65  		refs[i], err = parseRef(string(out))
    66  		if err != nil {
    67  			return nil, fmt.Errorf(
    68  				"containers.Push: failed to parse pushed reference for %s: %w",
    69  				t, err,
    70  			)
    71  		}
    72  	}
    73  
    74  	return refs, nil
    75  }
    76  
    77  // parseRef parses an OCI reference from the output from a container pushing
    78  // rule.
    79  func parseRef(out string) (name.Digest, error) {
    80  	// rules_oci prints the ref out in the middle of the string
    81  	// parse the text line by line and look for a line and find a valid image
    82  	// see pkg/tools/hack/containers/testdata for example logs
    83  	for _, line := range strings.Split(strings.TrimSuffix(out, "\n"), "\n") {
    84  		digest, err := name.NewDigest(line)
    85  		if err == nil {
    86  			return digest, nil
    87  		}
    88  	}
    89  
    90  	// if we didnt find anything with the above its probably rules_docker
    91  	// Get last space separated element of output, to drop noise from rules_docker
    92  	//   ❯ just push test/rosa/...
    93  	//     ...
    94  	//   INFO: Running command line: bazel-bin/test/rosa/container_push
    95  	//   2024/02/01 15:2... - us-east1-docker.pkg.dev/ret-edge-pltf-infra/workloads/lumperctl@sha256:d98d67
    96  	tokens := strings.Split(strings.TrimSpace(strings.ReplaceAll(out, "\r\n", " ")), " ")
    97  	return name.NewDigest(tokens[len(tokens)-1])
    98  }
    99  
   100  // QueryPushes queries Bazel for all container_push targets within the set of
   101  // targets in the expression.
   102  func QueryPushes(expr ...string) ([]string, error) {
   103  	pushes, err := bazel.QueryFromFile(fmt.Sprintf(
   104  		"kind('container_push2', '%s')", strings.Join(expr, " "),
   105  	))
   106  	if err != nil {
   107  		return nil, fmt.Errorf("containers.QueryPushes: failed to query for container_push targets. error: %w", err)
   108  	}
   109  	return pushes, nil
   110  }
   111  
   112  // Build builds one or more push targets without actually pushing. This produces
   113  // a digest that can be used for automation / processing without a direct
   114  // dependency on a remote OCI registry.
   115  func Build(bzl *bazel.Bazel, targets []string, opts ...Option) error {
   116  	options := makeOptions(opts...)
   117  
   118  	if err := bzl.Build(append(targets, options.args()...)...).Run(); err != nil {
   119  		return fmt.Errorf("containers.Build: bazel build failed. error: %w", err)
   120  	}
   121  
   122  	return nil
   123  }
   124  
   125  func pushHelp(registry string, err error) string {
   126  	switch {
   127  	// cant/dont provide guidance for non-GCP registries
   128  	case !strings.Contains(registry, garFrag) && !strings.Contains(registry, gcrFrag):
   129  		return ""
   130  	case strings.Contains(err.Error(), permissionErr):
   131  		helpText := ""
   132  		if strings.Contains(err.Error(), permissionErr) {
   133  			command := "gcloud"
   134  			args := []string{"auth", "list", "--filter=status:ACTIVE", "--format=value(account)"}
   135  			out, err := exec.Command(command, args...).CombinedOutput()
   136  			var acct string
   137  			if err != nil {
   138  				acct = fmt.Sprintf("unknown (error executing gcloud auth: %s)", err.Error())
   139  			} else {
   140  				acct = strings.TrimSpace(string(out))
   141  			}
   142  
   143  			cmd := fmt.Sprintf("gcloud auth configure-docker %s", registry)
   144  			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)
   145  		}
   146  		return helpText
   147  	default:
   148  		return ""
   149  	}
   150  }
   151  

View as plain text