diff --git a/go.mod b/go.mod index b7b7533f4..8221fa982 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.26.0 require ( buf.build/gen/go/namespace/cloud/connectrpc/go v1.20.0-20260610154328-540a8ce9853d.1 - buf.build/gen/go/namespace/cloud/grpc/go v1.6.2-20260610154328-540a8ce9853d.1 - buf.build/gen/go/namespace/cloud/protocolbuffers/go v1.36.11-20260623071207-0391158f5cec.1 + buf.build/gen/go/namespace/cloud/grpc/go v1.6.2-20260624153431-8e77f0a824d9.1 + buf.build/gen/go/namespace/cloud/protocolbuffers/go v1.36.11-20260624153431-8e77f0a824d9.1 cloud.google.com/go/artifactregistry v1.17.2 cloud.google.com/go/container v1.45.0 connectrpc.com/connect v1.20.0 diff --git a/go.sum b/go.sum index df63d93c1..dbb4e2437 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ buf.build/gen/go/namespace/cloud/connectrpc/go v1.20.0-20260610154328-540a8ce9853d.1 h1:BMQ7VNoTTpLstAIzGzYJFbNtOTEO7m3qcfnMr/k7x6k= buf.build/gen/go/namespace/cloud/connectrpc/go v1.20.0-20260610154328-540a8ce9853d.1/go.mod h1:hvHz7jMtG0TUy5XmDQcxhXNhMElaj0SU5TOSpx5HFyQ= -buf.build/gen/go/namespace/cloud/grpc/go v1.6.2-20260610154328-540a8ce9853d.1 h1:Nxs0ekOZSf4+yzTT0+KO9dFAmmHiPvf8N/7TruOB+WI= -buf.build/gen/go/namespace/cloud/grpc/go v1.6.2-20260610154328-540a8ce9853d.1/go.mod h1:S5jbhx/UZDUUajqANfQAjXL/qeEYMIY8VJsubujburI= -buf.build/gen/go/namespace/cloud/protocolbuffers/go v1.36.11-20260623071207-0391158f5cec.1 h1:iVj1+NtQo1x66y4xbeqyVXWwSBZ1Rz19kK+wICD1+Ic= -buf.build/gen/go/namespace/cloud/protocolbuffers/go v1.36.11-20260623071207-0391158f5cec.1/go.mod h1:Il2wpJNQB40Yj3Rmuhg5xKJPSXaZVwij+Q30d1PNuNY= +buf.build/gen/go/namespace/cloud/grpc/go v1.6.2-20260624153431-8e77f0a824d9.1 h1:7YEPBp1+4VodLK7gvn67225DKDB1xBC4fKijiZeTPx0= +buf.build/gen/go/namespace/cloud/grpc/go v1.6.2-20260624153431-8e77f0a824d9.1/go.mod h1:7D6jFYSgFIvJzSSOMi1wX+blvL5dGuC1qeWacY7vOxI= +buf.build/gen/go/namespace/cloud/protocolbuffers/go v1.36.11-20260624153431-8e77f0a824d9.1 h1:CDPUKLfM677Z/trVVQ8E2eybJIsLxLOblEkuo7q0RAc= +buf.build/gen/go/namespace/cloud/protocolbuffers/go v1.36.11-20260624153431-8e77f0a824d9.1/go.mod h1:Il2wpJNQB40Yj3Rmuhg5xKJPSXaZVwij+Q30d1PNuNY= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= diff --git a/internal/cli/cmd/cluster/artifact.go b/internal/cli/cmd/cluster/artifact.go index 51a296185..a5c0b56a0 100644 --- a/internal/cli/cmd/cluster/artifact.go +++ b/internal/cli/cmd/cluster/artifact.go @@ -27,6 +27,7 @@ import ( "github.com/spf13/pflag" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" "namespacelabs.dev/foundation/framework/io/downloader" "namespacelabs.dev/foundation/internal/cli/fncobra" "namespacelabs.dev/foundation/internal/console" @@ -50,6 +51,7 @@ func NewArtifactCmd() *cobra.Command { cmd.AddCommand(newArtifactDownloadCmd()) cmd.AddCommand(newArtifactCacheURLCmd()) cmd.AddCommand(newArtifactExpireCmd()) + cmd.AddCommand(newArtifactExtendCmd()) cmd.AddCommand(newArtifactDescribeCmd()) return cmd @@ -346,6 +348,82 @@ func newArtifactExpireCmd() *cobra.Command { }) } +func newArtifactExtendCmd() *cobra.Command { + var namespace string + var extendBy, ensureMinimum time.Duration + var extendByFlag, ensureMinimumFlag *pflag.Flag + + return fncobra.Cmd(&cobra.Command{ + Use: "extend [path]", + Short: "Extend the expiration of an artifact.", + Long: `Extend the expiration of a finalized artifact. + +This enables dynamic, access-based retention: create artifacts with a short +expiration and push it out whenever they are accessed. + +At least one of --by or --ensure_minimum must be set. When both are set, --by is +applied first and --ensure_minimum then acts as a lower bound on the result.`, + Args: cobra.ExactArgs(1), + }).WithFlags(func(flags *pflag.FlagSet) { + flags.StringVar(&namespace, "namespace", mainArtifactNamespace, "Namespace of the artifact.") + fncobra.DurationVar(flags, &extendBy, "by", 0, "Extend the current expiration by this duration, relative to the artifact's current expiration.") + fncobra.DurationVar(flags, &ensureMinimum, "ensure_minimum", 0, "Ensure the artifact expires no sooner than this duration from now.") + extendByFlag = flags.Lookup("by") + ensureMinimumFlag = flags.Lookup("ensure_minimum") + }).DoWithArgs(func(ctx context.Context, args []string) error { + path := args[0] + + if !extendByFlag.Changed && !ensureMinimumFlag.Changed { + return fnerrors.BadInputError("at least one of --by or --ensure_minimum must be set") + } + + req := &storagev1beta.ExtendArtifactRequest{ + Path: path, + Namespace: namespace, + } + + if extendByFlag.Changed { + if extendBy <= 0 { + return fnerrors.BadInputError("--by must be positive") + } + req.ExtendBy = durationpb.New(extendBy) + } + + if ensureMinimumFlag.Changed { + if ensureMinimum <= 0 { + return fnerrors.BadInputError("--ensure_minimum must be positive") + } + req.EnsureMinimum = durationpb.New(ensureMinimum) + } + + token, err := auth.LoadDefaults() + if err != nil { + return err + } + + cli, err := storage.NewClient(ctx, token) + if err != nil { + return err + } + defer cli.Close() + + res, err := cli.Artifacts.ExtendArtifact(ctx, req) + if err != nil { + return err + } + + if res.GetExpiresAt() != nil { + fmt.Fprintf(console.Stdout(ctx), "Extended %s (namespace %s); now expires %s.\n", + path, namespace, res.GetExpiresAt().AsTime().Format(time.RFC3339)) + } else { + fmt.Fprintf(console.Stdout(ctx), "Extended %s (namespace %s); now never expires.\n", + path, namespace) + } + + return nil + }) +} + func newArtifactDownloadCmd() *cobra.Command { var namespace string var resume, unpack bool