1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-18 22:44:00 +03:00
regclient/mod/config.go
Brandon Mitchell 95c9152941
Chore: Modernize Go to the 1.22 specs
- Use "any" instead of an empty interface.
- Use range over an integer for for loops.
- Remove shadow variables in loops now that Go no longer reuses the variable.
- Use "slices.Contains", "slices.Delete", "slices.Equal", "slices.Index", "slices.SortFunc".
- Use "cmp.Or", "min", and "max".
- Use "fmt.Appendf" instead of "Sprintf" for generating a byte slice.
- Use "errors.Join" or "fmt.Errorf" with multiple "%w" for multiple errors.

Additionally, use modern regclient features:

- Use "ref.SetTag", "ref.SetDigest", and "ref.AddDigest".
- Call "regclient.ManifestGet" using "WithManifestDesc" instead of setting the digest on the reference.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
2025-02-18 14:32:06 -05:00

505 lines
15 KiB
Go

package mod
import (
"context"
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/opencontainers/go-digest"
"github.com/regclient/regclient"
"github.com/regclient/regclient/types/blob"
"github.com/regclient/regclient/types/platform"
"github.com/regclient/regclient/types/ref"
)
// WithBuildArgRm removes a build arg from the config history.
func WithBuildArgRm(arg string, value *regexp.Regexp) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
changed := false
oc := doc.oc.GetConfig()
argexp := regexp.MustCompile(fmt.Sprintf(`(?s)^ARG %s(=.*|)$`,
regexp.QuoteMeta(arg)))
runexp := regexp.MustCompile(fmt.Sprintf(`(?s)^RUN \|([0-9]+) (.*)%s=%s(.*)$`,
regexp.QuoteMeta(arg), value.String()))
for i := len(oc.History) - 1; i >= 0; i-- {
if argexp.MatchString(oc.History[i].CreatedBy) && oc.History[i].EmptyLayer {
// delete empty build arg history entry
oc.History = slices.Delete(oc.History, i, i+1)
changed = true
} else if match := runexp.FindStringSubmatch(oc.History[i].CreatedBy); len(match) == 4 {
// delete arg from run steps
count, err := strconv.Atoi(match[1])
if err != nil {
return fmt.Errorf("failed parsing history \"%s\": %w", oc.History[i].CreatedBy, err)
}
if count == 1 && len(match[2]) == 0 {
oc.History[i].CreatedBy = fmt.Sprintf("RUN %s", match[3])
} else {
oc.History[i].CreatedBy = fmt.Sprintf("RUN |%d %s%s", count-1, match[2], match[3])
}
changed = true
}
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
}
return nil
})
return nil
}
}
// WithConfigCmd sets the command in the config.
// For running a shell command, the `cmd` value should be `[]string{"/bin/sh", "-c", command}`.
func WithConfigCmd(cmd []string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
oc := doc.oc.GetConfig()
if slices.Equal(cmd, oc.Config.Cmd) {
return nil
}
oc.Config.Cmd = cmd
doc.oc.SetConfig(oc)
doc.modified = true
return nil
})
return nil
}
}
// WithConfigDigestAlgo changes the digest algorithm.
func WithConfigDigestAlgo(algo digest.Algorithm) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
if !algo.Available() {
return fmt.Errorf("digest algorithm is not available: %s", string(algo))
}
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
desc := doc.oc.GetDescriptor()
if doc.newDesc.MediaType != "" {
desc = doc.newDesc
}
if desc.DigestAlgo() == algo {
return nil
}
if !algo.Available() {
return fmt.Errorf("unavailable digest algorithm: %s", string(algo))
}
body, err := doc.oc.RawBody()
if err != nil {
return fmt.Errorf("failed to get config body: %w", err)
}
desc.Digest = algo.FromBytes(body)
doc.oc = blob.NewOCIConfig(
blob.WithDesc(desc),
blob.WithRawBody(body),
)
doc.modified = true
return nil
})
return nil
}
}
// WithConfigEntrypoint sets the entrypoint in the config.
// For running a shell command, the `entrypoint` value should be `[]string{"/bin/sh", "-c", command}`.
func WithConfigEntrypoint(entrypoint []string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
oc := doc.oc.GetConfig()
if slices.Equal(entrypoint, oc.Config.Entrypoint) {
return nil
}
oc.Config.Entrypoint = entrypoint
doc.oc.SetConfig(oc)
doc.modified = true
return nil
})
return nil
}
}
// WithConfigPlatform sets the platform in the config.
func WithConfigPlatform(p platform.Platform) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
oc := doc.oc.GetConfig()
if platform.Match(oc.Platform, p) {
return nil
}
oc.Platform = p
doc.oc.SetConfig(oc)
doc.modified = true
return nil
})
return nil
}
}
// WithConfigTimestamp sets the timestamp on the config entries based on options.
func WithConfigTimestamp(optTime OptTime) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
if optTime.Set.IsZero() && optTime.FromLabel == "" {
return fmt.Errorf("WithConfigTimestamp requires a time to set")
}
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(c context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
oc := doc.oc.GetConfig()
// lookup start time from label
if optTime.FromLabel != "" {
tl, ok := oc.Config.Labels[optTime.FromLabel]
if !ok {
return fmt.Errorf("label not found: %s", optTime.FromLabel)
}
tNew, err := time.Parse(time.RFC3339, tl)
if err != nil {
// TODO: add fallbacks
return fmt.Errorf("could not parse time %s from %s: %w", tl, optTime.FromLabel, err)
}
if !optTime.Set.IsZero() && !optTime.Set.Equal(tNew) {
return fmt.Errorf("conflicting time labels found %s and %s", optTime.Set.String(), tNew.String())
}
optTime.Set = tNew
}
startHistory := 0
// offset startHistory by base layer count
if optTime.BaseLayers > 0 {
layersCount := 0
for i, history := range oc.History {
if !history.EmptyLayer {
layersCount++
}
if layersCount == optTime.BaseLayers {
startHistory = i + 1
break
}
}
if startHistory == 0 {
startHistory = len(oc.History)
}
}
// offset startHistory from base image history
if !optTime.BaseRef.IsZero() {
baseConfig, err := rc.ImageConfig(c, optTime.BaseRef, regclient.ImageWithPlatform(oc.Platform.String()))
if err != nil {
return fmt.Errorf("failed to get base image config: %w", err)
}
// exclude matching history lines from base image
for i, history := range baseConfig.GetConfig().History {
if len(oc.History) <= i ||
oc.History[i].Author != history.Author ||
oc.History[i].Comment != history.Comment ||
!oc.History[i].Created.Equal(*history.Created) ||
oc.History[i].CreatedBy != history.CreatedBy ||
oc.History[i].EmptyLayer != history.EmptyLayer {
break
}
startHistory = i + 1
}
}
var changed, cCur bool
// adjust created time on created and history fields
if oc.Created != nil {
*oc.Created, changed = timeModOpt(*oc.Created, optTime)
}
for i := startHistory; i < len(oc.History); i++ {
*oc.History[i].Created, cCur = timeModOpt(*oc.History[i].Created, optTime)
changed = changed || cCur
}
if changed {
doc.oc.SetConfig(oc)
doc.newDesc = doc.oc.GetDescriptor()
doc.modified = true
}
return nil
})
return nil
}
}
// WithConfigTimestampFromLabel sets the max timestamp in the config to match a label value.
//
// Deprecated: replace with [WithConfigTimestamp].
func WithConfigTimestampFromLabel(label string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(c context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
var err error
changed := false
oc := doc.oc.GetConfig()
tl, ok := oc.Config.Labels[label]
if !ok {
return fmt.Errorf("label not found: %s", label)
}
t, err := time.Parse(time.RFC3339, tl)
if err != nil {
// TODO: add fallbacks
return fmt.Errorf("could not parse time %s from %s: %w", tl, label, err)
}
if oc.Created != nil && t.Before(*oc.Created) {
*oc.Created = t
changed = true
}
if oc.History != nil {
for i, h := range oc.History {
if h.Created != nil && t.Before(*h.Created) {
*oc.History[i].Created = t
changed = true
}
}
}
if changed {
doc.oc.SetConfig(oc)
doc.newDesc = doc.oc.GetDescriptor()
doc.modified = true
}
return nil
})
return nil
}
}
// WithConfigTimestampMax sets the max timestamp on any config objects.
//
// Deprecated: replace with [WithConfigTimestamp].
func WithConfigTimestampMax(t time.Time) Opts {
return WithConfigTimestamp(OptTime{
Set: t,
After: t,
})
}
// WithEnv sets or deletes an environment variable from the image config.
func WithEnv(name, value string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
// extract the list for platforms to update from the name
name = strings.TrimSpace(name)
platforms := []platform.Platform{}
if name[0] == '[' && strings.Index(name, "]") > 0 {
end := strings.Index(name, "]")
list := strings.Split(name[1:end], ",")
for _, entry := range list {
entry = strings.TrimSpace(entry)
if entry == "*" {
continue
}
p, err := platform.Parse(entry)
if err != nil {
return fmt.Errorf("failed to parse env platform %s: %w", entry, err)
}
platforms = append(platforms, p)
}
name = name[end+1:]
}
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(c context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
// if platforms are listed, skip non-matching platforms
if len(platforms) > 0 {
p := doc.oc.GetConfig().Platform
found := false
for _, pe := range platforms {
if platform.Match(p, pe) {
found = true
break
}
}
if !found {
return nil
}
}
changed := false
found := false
oc := doc.oc.GetConfig()
for i, kv := range oc.Config.Env {
kvSplit := strings.SplitN(kv, "=", 2)
if kvSplit[0] != name {
continue
}
found = true
if value == "" {
// delete an entry
oc.Config.Env = slices.Delete(oc.Config.Env, i, i+1)
changed = true
} else if len(kvSplit) < 2 || value != kvSplit[1] {
// change an entry
oc.Config.Env[i] = name + "=" + value
changed = true
}
break
}
if !found && value != "" {
// add a new entry
oc.Config.Env = append(oc.Config.Env, name+"="+value)
changed = true
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
doc.newDesc = doc.oc.GetDescriptor()
}
return nil
})
return nil
}
}
// WithExposeAdd defines an exposed port in the image config.
func WithExposeAdd(port string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
changed := false
oc := doc.oc.GetConfig()
if oc.Config.ExposedPorts == nil {
oc.Config.ExposedPorts = map[string]struct{}{}
}
if _, ok := oc.Config.ExposedPorts[port]; !ok {
changed = true
oc.Config.ExposedPorts[port] = struct{}{}
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
doc.newDesc = doc.oc.GetDescriptor()
}
return nil
})
return nil
}
}
// WithExposeRm deletes an exposed from the image config.
func WithExposeRm(port string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
changed := false
oc := doc.oc.GetConfig()
if oc.Config.ExposedPorts == nil {
return nil
}
if _, ok := oc.Config.ExposedPorts[port]; ok {
changed = true
delete(oc.Config.ExposedPorts, port)
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
doc.newDesc = doc.oc.GetDescriptor()
}
return nil
})
return nil
}
}
// WithLabel sets or deletes a label from the image config.
func WithLabel(name, value string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
// extract the list for platforms to update from the name
name = strings.TrimSpace(name)
platforms := []platform.Platform{}
if name[0] == '[' && strings.Index(name, "]") > 0 {
end := strings.Index(name, "]")
list := strings.Split(name[1:end], ",")
for _, entry := range list {
entry = strings.TrimSpace(entry)
if entry == "*" {
continue
}
p, err := platform.Parse(entry)
if err != nil {
return fmt.Errorf("failed to parse label platform %s: %w", entry, err)
}
platforms = append(platforms, p)
}
name = name[end+1:]
}
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(c context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
// if platforms are listed, skip non-matching platforms
if len(platforms) > 0 {
p := doc.oc.GetConfig().Platform
found := false
for _, pe := range platforms {
if platform.Match(p, pe) {
found = true
break
}
}
if !found {
return nil
}
}
changed := false
oc := doc.oc.GetConfig()
if oc.Config.Labels == nil {
oc.Config.Labels = map[string]string{}
}
cur, ok := oc.Config.Labels[name]
if value == "" && ok {
delete(oc.Config.Labels, name)
changed = true
} else if value != "" && value != cur {
oc.Config.Labels[name] = value
changed = true
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
doc.newDesc = doc.oc.GetDescriptor()
}
return nil
})
return nil
}
}
// WithVolumeAdd defines a volume in the image config.
func WithVolumeAdd(volume string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
changed := false
oc := doc.oc.GetConfig()
if oc.Config.Volumes == nil {
oc.Config.Volumes = map[string]struct{}{}
}
if _, ok := oc.Config.Volumes[volume]; !ok {
changed = true
oc.Config.Volumes[volume] = struct{}{}
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
doc.newDesc = doc.oc.GetDescriptor()
}
return nil
})
return nil
}
}
// WithVolumeRm deletes a volume from the image config.
func WithVolumeRm(volume string) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.stepsOCIConfig = append(dc.stepsOCIConfig, func(ctx context.Context, rc *regclient.RegClient, rSrc, rTgt ref.Ref, doc *dagOCIConfig) error {
changed := false
oc := doc.oc.GetConfig()
if oc.Config.Volumes == nil {
return nil
}
if _, ok := oc.Config.Volumes[volume]; ok {
changed = true
delete(oc.Config.Volumes, volume)
}
if changed {
doc.oc.SetConfig(oc)
doc.modified = true
doc.newDesc = doc.oc.GetDescriptor()
}
return nil
})
return nil
}
}