@@ -2,6 +2,7 @@ package provider
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67
78 "github.com/chainguard-dev/terraform-provider-oci/pkg/validators"
@@ -10,10 +11,13 @@ import (
1011 "github.com/hashicorp/terraform-plugin-framework/path"
1112 "github.com/hashicorp/terraform-plugin-framework/resource"
1213 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
14+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
1315 "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1416 "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1517 "github.com/hashicorp/terraform-plugin-framework/schema/validator"
1618 "github.com/hashicorp/terraform-plugin-framework/types"
19+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
20+ "golang.org/x/sync/errgroup"
1721)
1822
1923var _ resource.Resource = & TagResource {}
@@ -35,6 +39,7 @@ type TagResourceModel struct {
3539
3640 DigestRef types.String `tfsdk:"digest_ref"`
3741 Tag types.String `tfsdk:"tag"`
42+ Tags []string `tfsdk:"tags"`
3843}
3944
4045func (r * TagResource ) Metadata (ctx context.Context , req resource.MetadataRequest , resp * resource.MetadataResponse ) {
@@ -53,11 +58,19 @@ func (r *TagResource) Schema(ctx context.Context, req resource.SchemaRequest, re
5358 },
5459 "tag" : schema.StringAttribute {
5560 MarkdownDescription : "Tag to apply to the image." ,
56- Required : true ,
61+ Optional : true ,
5762 Validators : []validator.String {validators.TagValidator {}},
5863 PlanModifiers : []planmodifier.String {stringplanmodifier .RequiresReplace ()},
64+ DeprecationMessage : "The `tag` attribute is deprecated. Use `tags` instead." ,
65+ },
66+ "tags" : schema.ListAttribute {
67+ MarkdownDescription : "Tags to apply to the image." ,
68+ // TODO: make this required after tag deprecation period.
69+ Optional : true ,
70+ ElementType : basetypes.StringType {},
71+ Validators : []validator.List {uniqueTagsValidator {}},
72+ PlanModifiers : []planmodifier.List {listplanmodifier .RequiresReplace ()},
5973 },
60-
6174 "tagged_ref" : schema.StringAttribute {
6275 Computed : true ,
6376 MarkdownDescription : "The resulting fully-qualified image ref by digest (e.g. {repo}:tag@sha256:deadbeef)." ,
@@ -113,8 +126,8 @@ func (r *TagResource) Read(ctx context.Context, req resource.ReadRequest, resp *
113126 return
114127 }
115128
116- // Don't actually tag, but check whether the digest is already tagged so we get a useful diff.
117- // If the digest is already tagged, we'll set the ID and tagged_ref to the correct output value.
129+ // Don't actually tag, but check whether the digest is already tagged with all requested tags, so we get a useful diff.
130+ // If the digest is already tagged with all requested tags , we'll set the ID and tagged_ref to the correct output value.
118131 // Otherwise, we'll set them to empty strings so that the create will run when applied.
119132
120133 d , err := name .NewDigest (data .DigestRef .ValueString ())
@@ -123,22 +136,38 @@ func (r *TagResource) Read(ctx context.Context, req resource.ReadRequest, resp *
123136 return
124137 }
125138
126- t := d .Context ().Tag (data .Tag .ValueString ())
127- desc , err := remote .Get (t , r .popts .withContext (ctx )... )
128- if err != nil {
129- resp .Diagnostics .AddError ("Tag Error" , fmt .Sprintf ("Error getting image: %s" , err .Error ()))
130- return
139+ tags := []string {}
140+ if data .Tag .ValueString () != "" {
141+ tags = append (tags , data .Tag .ValueString ())
142+ } else if len (data .Tags ) > 0 {
143+ tags = data .Tags
144+ } else {
145+ resp .Diagnostics .AddError ("Tag Error" , "either tag or tags must be set" )
146+ }
147+ if data .Tag .ValueString () != "" && len (data .Tags ) > 0 {
148+ resp .Diagnostics .AddError ("Tag Error" , "only one of tag or tags may be set" )
131149 }
150+ for _ , tag := range tags {
151+ t := d .Context ().Tag (tag )
152+ desc , err := remote .Get (t , r .popts .withContext (ctx )... )
153+ if err != nil {
154+ // Failed to get the image by tag, so we need to create.
155+ return
156+ }
132157
133- if desc .Digest .String () != d .DigestStr () {
134- data .Id = types .StringValue ("" )
135- data .TaggedRef = types .StringValue ("" )
136- } else {
137- id := fmt .Sprintf ("%s@%s" , t .Name (), desc .Digest .String ())
138- data .Id = types .StringValue (id )
139- data .TaggedRef = types .StringValue (id )
158+ // Some tag is wrong, so we need to create.
159+ if desc .Digest .String () != d .DigestStr () {
160+ data .Id = types .StringValue ("" )
161+ data .TaggedRef = types .StringValue ("" )
162+ break
163+ }
140164 }
141165
166+ // All tags are correct so we can set the ID and tagged_ref to the correct output value.
167+ id := fmt .Sprintf ("%s@%s" , d .Context ().Tag (tags [0 ]), d .DigestStr ())
168+ data .Id = types .StringValue (id )
169+ data .TaggedRef = types .StringValue (id )
170+
142171 // Save updated data into Terraform state
143172 resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
144173}
@@ -171,21 +200,83 @@ func (r *TagResource) ImportState(ctx context.Context, req resource.ImportStateR
171200}
172201
173202func (r * TagResource ) doTag (ctx context.Context , data * TagResourceModel ) (string , error ) {
203+ var tags []string
204+ if data .Tag .ValueString () != "" {
205+ tags = append (tags , data .Tag .ValueString ())
206+ } else if len (data .Tags ) > 0 {
207+ tags = data .Tags
208+ } else {
209+ return "" , errors .New ("either tag or tags must be set" )
210+ }
211+ if data .Tag .ValueString () != "" && len (data .Tags ) > 0 {
212+ return "" , errors .New ("only one of tag or tags may be set" )
213+ }
214+
174215 d , err := name .NewDigest (data .DigestRef .ValueString ())
175216 if err != nil {
176217 return "" , fmt .Errorf ("digest_ref must be a digest reference: %v" , err )
177218 }
178- t := d .Context ().Tag (data .Tag .ValueString ())
179- if err != nil {
180- return "" , fmt .Errorf ("error parsing tag: %v" , err )
181- }
219+
182220 desc , err := remote .Get (d , r .popts .withContext (ctx )... )
183221 if err != nil {
184222 return "" , fmt .Errorf ("error fetching digest: %v" , err )
185223 }
186- if err := remote .Tag (t , desc , r .popts .withContext (ctx )... ); err != nil {
187- return "" , fmt .Errorf ("error tagging digest: %v" , err )
224+
225+ errg , ctx := errgroup .WithContext (ctx )
226+ for _ , tag := range tags {
227+ tag := tag
228+ errg .Go (func () error {
229+ t := d .Context ().Tag (tag )
230+ if err != nil {
231+ return fmt .Errorf ("error parsing tag %q: %v" , tag , err )
232+ }
233+ if err := remote .Tag (t , desc , r .popts .withContext (ctx )... ); err != nil {
234+ return fmt .Errorf ("error tagging digest with %q: %v" , tag , err )
235+ }
236+ return nil
237+ })
238+ }
239+ if err := errg .Wait (); err != nil {
240+ return "" , err
241+ }
242+
243+ t := d .Context ().Tag (tags [0 ])
244+ if err != nil {
245+ return "" , fmt .Errorf ("error parsing tag: %v" , err )
188246 }
189- digest := fmt .Sprintf ("%s@%s" , t .Name (), desc . Digest . String ())
247+ digest := fmt .Sprintf ("%s@%s" , t .Name (), d . DigestStr ())
190248 return digest , nil
191249}
250+
251+ type uniqueTagsValidator struct {}
252+
253+ var _ validator.List = uniqueTagsValidator {}
254+
255+ func (v uniqueTagsValidator ) Description (context.Context ) string {
256+ return `value must be valid OCI tag elements (e.g., "latest", "v1.2.3")`
257+ }
258+ func (v uniqueTagsValidator ) MarkdownDescription (ctx context.Context ) string {
259+ return v .Description (ctx )
260+ }
261+
262+ func (v uniqueTagsValidator ) ValidateList (ctx context.Context , req validator.ListRequest , resp * validator.ListResponse ) {
263+ if req .ConfigValue .IsNull () || req .ConfigValue .IsUnknown () {
264+ return
265+ }
266+ var tags []string
267+ if diag := req .ConfigValue .ElementsAs (ctx , & tags , false ); diag .HasError () {
268+ resp .Diagnostics .Append (diag ... )
269+ return
270+ }
271+
272+ seen := map [string ]bool {}
273+ for _ , t := range tags {
274+ if seen [t ] {
275+ resp .Diagnostics .AddWarning ("Duplicate tag" , fmt .Sprintf ("duplicate tag %q" , t ))
276+ }
277+ seen [t ] = true
278+ if _ , err := name .NewTag ("example.com:" + t ); err != nil {
279+ resp .Diagnostics .AddError ("Invalid OCI tag name" , fmt .Sprintf ("parsing tag %q: %v" , t , err ))
280+ }
281+ }
282+ }
0 commit comments