package buildflags import ( "context" "encoding/json" "maps" "os" "strconv" "strings" awsconfig "github.com/aws/aws-sdk-go-v2/config" controllerapi "github.com/docker/buildx/controller/pb" "github.com/pkg/errors" "github.com/tonistiigi/go-csvvalue" "github.com/zclconf/go-cty/cty" jsoncty "github.com/zclconf/go-cty/cty/json" ) type CacheOptions []*CacheOptionsEntry func (o CacheOptions) Merge(other CacheOptions) CacheOptions { if other == nil { return o.Normalize() } else if o == nil { return other.Normalize() } return append(o, other...).Normalize() } func (o CacheOptions) Normalize() CacheOptions { if len(o) == 0 { return nil } return removeDupes(o) } func (o CacheOptions) ToPB() []*controllerapi.CacheOptionsEntry { if len(o) == 0 { return nil } var outs []*controllerapi.CacheOptionsEntry for _, entry := range o { pb := entry.ToPB() if !isActive(pb) { continue } outs = append(outs, pb) } return outs } type CacheOptionsEntry struct { Type string `json:"type"` Attrs map[string]string `json:"attrs,omitempty"` } func (e *CacheOptionsEntry) Equal(other *CacheOptionsEntry) bool { if e.Type != other.Type { return false } return maps.Equal(e.Attrs, other.Attrs) } func (e *CacheOptionsEntry) String() string { // Special registry syntax. if e.Type == "registry" && len(e.Attrs) == 1 { if ref, ok := e.Attrs["ref"]; ok { return ref } } var b csvBuilder if e.Type != "" { b.Write("type", e.Type) } if len(e.Attrs) > 0 { b.WriteAttributes(e.Attrs) } return b.String() } func (e *CacheOptionsEntry) ToPB() *controllerapi.CacheOptionsEntry { ci := &controllerapi.CacheOptionsEntry{ Type: e.Type, Attrs: maps.Clone(e.Attrs), } addGithubToken(ci) addAwsCredentials(ci) return ci } func (e *CacheOptionsEntry) MarshalJSON() ([]byte, error) { m := maps.Clone(e.Attrs) if m == nil { m = map[string]string{} } m["type"] = e.Type return json.Marshal(m) } func (e *CacheOptionsEntry) UnmarshalJSON(data []byte) error { var m map[string]string if err := json.Unmarshal(data, &m); err != nil { return err } e.Type = m["type"] delete(m, "type") e.Attrs = m return e.validate(data) } func (e *CacheOptionsEntry) UnmarshalText(text []byte) error { in := string(text) fields, err := csvvalue.Fields(in, nil) if err != nil { return err } if len(fields) == 1 && !strings.Contains(fields[0], "=") { e.Type = "registry" e.Attrs = map[string]string{"ref": fields[0]} return nil } e.Type = "" e.Attrs = map[string]string{} for _, field := range fields { parts := strings.SplitN(field, "=", 2) if len(parts) != 2 { return errors.Errorf("invalid value %s", field) } key := strings.ToLower(parts[0]) value := parts[1] switch key { case "type": e.Type = value default: e.Attrs[key] = value } } if e.Type == "" { return errors.Errorf("type required form> %q", in) } return e.validate(text) } func (e *CacheOptionsEntry) validate(gv any) error { if e.Type == "" { var text []byte switch gv := gv.(type) { case []byte: text = gv case string: text = []byte(gv) case cty.Value: text, _ = jsoncty.Marshal(gv, gv.Type()) default: text, _ = json.Marshal(gv) } return errors.Errorf("type required form> %q", string(text)) } return nil } func ParseCacheEntry(in []string) (CacheOptions, error) { if len(in) == 0 { return nil, nil } opts := make(CacheOptions, 0, len(in)) for _, in := range in { if in == "" { continue } if !strings.Contains(in, "=") { // This is ref only format. Each field in the CSV is its own entry. fields, err := csvvalue.Fields(in, nil) if err != nil { return nil, err } for _, field := range fields { opt := CacheOptionsEntry{} if err := opt.UnmarshalText([]byte(field)); err != nil { return nil, err } opts = append(opts, &opt) } continue } var out CacheOptionsEntry if err := out.UnmarshalText([]byte(in)); err != nil { return nil, err } opts = append(opts, &out) } return opts, nil } func addGithubToken(ci *controllerapi.CacheOptionsEntry) { if ci.Type != "gha" { return } version, ok := ci.Attrs["version"] if !ok { // https://github.com/actions/toolkit/blob/2b08dc18f261b9fdd978b70279b85cbef81af8bc/packages/cache/src/internal/config.ts#L19 if v, ok := os.LookupEnv("ACTIONS_CACHE_SERVICE_V2"); ok { if b, err := strconv.ParseBool(v); err == nil && b { version = "2" } } } if _, ok := ci.Attrs["token"]; !ok { if v, ok := os.LookupEnv("ACTIONS_RUNTIME_TOKEN"); ok { ci.Attrs["token"] = v } } if _, ok := ci.Attrs["url_v2"]; !ok && version == "2" { // https://github.com/actions/toolkit/blob/2b08dc18f261b9fdd978b70279b85cbef81af8bc/packages/cache/src/internal/config.ts#L34-L35 if v, ok := os.LookupEnv("ACTIONS_RESULTS_URL"); ok { ci.Attrs["url_v2"] = v } } if _, ok := ci.Attrs["url"]; !ok { // https://github.com/actions/toolkit/blob/2b08dc18f261b9fdd978b70279b85cbef81af8bc/packages/cache/src/internal/config.ts#L28-L33 if v, ok := os.LookupEnv("ACTIONS_CACHE_URL"); ok { ci.Attrs["url"] = v } else if v, ok := os.LookupEnv("ACTIONS_RESULTS_URL"); ok { ci.Attrs["url"] = v } } } func addAwsCredentials(ci *controllerapi.CacheOptionsEntry) { if ci.Type != "s3" { return } _, okAccessKeyID := ci.Attrs["access_key_id"] _, okSecretAccessKey := ci.Attrs["secret_access_key"] // If the user provides access_key_id, secret_access_key, do not override the session token. if okAccessKeyID && okSecretAccessKey { return } ctx := context.TODO() awsConfig, err := awsconfig.LoadDefaultConfig(ctx) if err != nil { return } credentials, err := awsConfig.Credentials.Retrieve(ctx) if err != nil { return } if !okAccessKeyID && credentials.AccessKeyID != "" { ci.Attrs["access_key_id"] = credentials.AccessKeyID } if !okSecretAccessKey && credentials.SecretAccessKey != "" { ci.Attrs["secret_access_key"] = credentials.SecretAccessKey } if _, ok := ci.Attrs["session_token"]; !ok && credentials.SessionToken != "" { ci.Attrs["session_token"] = credentials.SessionToken } } func isActive(pb *controllerapi.CacheOptionsEntry) bool { // Always active if not gha. if pb.Type != "gha" { return true } return pb.Attrs["token"] != "" && (pb.Attrs["url"] != "" || pb.Attrs["url_v2"] != "") }