Skip to content

Commit d5451c1

Browse files
authored
Revert "tag: accept multiple tags per resource (#74)" (#76)
This reverts commit e4a57d9. Instead of this, we'll use a new resource, `oci_tags`. This un-deprecates `tag` and removes `tags`.
1 parent dba8f3c commit d5451c1

File tree

4 files changed

+37
-204
lines changed

4 files changed

+37
-204
lines changed

docs/resources/tag.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ Tag an existing image by digest.
1818
### Required
1919

2020
- `digest_ref` (String) Image ref by digest to apply the tag to.
21-
22-
### Optional
23-
24-
- `tag` (String, Deprecated) Tag to apply to the image.
25-
- `tags` (List of String) Tags to apply to the image.
21+
- `tag` (String) Tag to apply to the image.
2622

2723
### Read-Only
2824

internal/provider/tag_resource.go

Lines changed: 23 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package provider
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76

87
"github.com/chainguard-dev/terraform-provider-oci/pkg/validators"
@@ -11,13 +10,10 @@ import (
1110
"github.com/hashicorp/terraform-plugin-framework/path"
1211
"github.com/hashicorp/terraform-plugin-framework/resource"
1312
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
14-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
1513
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1614
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1715
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
1816
"github.com/hashicorp/terraform-plugin-framework/types"
19-
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
20-
"golang.org/x/sync/errgroup"
2117
)
2218

2319
var _ resource.Resource = &TagResource{}
@@ -39,7 +35,6 @@ type TagResourceModel struct {
3935

4036
DigestRef types.String `tfsdk:"digest_ref"`
4137
Tag types.String `tfsdk:"tag"`
42-
Tags []string `tfsdk:"tags"`
4338
}
4439

4540
func (r *TagResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
@@ -58,19 +53,11 @@ func (r *TagResource) Schema(ctx context.Context, req resource.SchemaRequest, re
5853
},
5954
"tag": schema.StringAttribute{
6055
MarkdownDescription: "Tag to apply to the image.",
61-
Optional: true,
56+
Required: true,
6257
Validators: []validator.String{validators.TagValidator{}},
6358
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()},
7359
},
60+
7461
"tagged_ref": schema.StringAttribute{
7562
Computed: true,
7663
MarkdownDescription: "The resulting fully-qualified image ref by digest (e.g. {repo}:tag@sha256:deadbeef).",
@@ -126,8 +113,8 @@ func (r *TagResource) Read(ctx context.Context, req resource.ReadRequest, resp *
126113
return
127114
}
128115

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.
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.
131118
// Otherwise, we'll set them to empty strings so that the create will run when applied.
132119

133120
d, err := name.NewDigest(data.DigestRef.ValueString())
@@ -136,38 +123,22 @@ func (r *TagResource) Read(ctx context.Context, req resource.ReadRequest, resp *
136123
return
137124
}
138125

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")
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
149131
}
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-
}
157132

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-
}
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)
164140
}
165141

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-
171142
// Save updated data into Terraform state
172143
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
173144
}
@@ -200,83 +171,21 @@ func (r *TagResource) ImportState(ctx context.Context, req resource.ImportStateR
200171
}
201172

202173
func (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-
215174
d, err := name.NewDigest(data.DigestRef.ValueString())
216175
if err != nil {
217176
return "", fmt.Errorf("digest_ref must be a digest reference: %v", err)
218177
}
219-
178+
t := d.Context().Tag(data.Tag.ValueString())
179+
if err != nil {
180+
return "", fmt.Errorf("error parsing tag: %v", err)
181+
}
220182
desc, err := remote.Get(d, r.popts.withContext(ctx)...)
221183
if err != nil {
222184
return "", fmt.Errorf("error fetching digest: %v", err)
223185
}
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)
186+
if err := remote.Tag(t, desc, r.popts.withContext(ctx)...); err != nil {
187+
return "", fmt.Errorf("error tagging digest: %v", err)
246188
}
247-
digest := fmt.Sprintf("%s@%s", t.Name(), d.DigestStr())
189+
digest := fmt.Sprintf("%s@%s", t.Name(), desc.Digest.String())
248190
return digest, nil
249191
}
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-
}

internal/provider/tag_resource_test.go

Lines changed: 12 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
func TestAccTagResource(t *testing.T) {
14-
repo, cleanup := ocitesting.SetupRepository(t, "repo")
14+
repo, cleanup := ocitesting.SetupRepository(t, "test")
1515
defer cleanup()
1616

1717
// Push an image to the local registry.
@@ -30,7 +30,7 @@ func TestAccTagResource(t *testing.T) {
3030
}
3131
dig1 := ref1.Context().Digest(d1.String())
3232

33-
// Push another image to the local registry.
33+
// Push an image to the local registry.
3434
ref2 := repo.Tag("2")
3535
t.Logf("Using ref2: %s", ref2)
3636
img2, err := random.Image(1024, 1)
@@ -46,108 +46,36 @@ func TestAccTagResource(t *testing.T) {
4646
}
4747
dig2 := ref2.Context().Digest(d2.String())
4848

49-
// Create the resource tagging dig1 three tags.
50-
want1 := fmt.Sprintf("%s:foo@%s", repo, d1)
49+
want1 := fmt.Sprintf("%s:test@%s", repo, d2)
50+
want2 := fmt.Sprintf("%s:test2@%s", repo, d2)
51+
5152
resource.Test(t, resource.TestCase{
5253
PreCheck: func() { testAccPreCheck(t) },
5354
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
5455
Steps: []resource.TestStep{
56+
// Create and Read testing
5557
{
5658
Config: fmt.Sprintf(`resource "oci_tag" "test" {
5759
digest_ref = %q
58-
tags = ["foo", "bar", "baz"]
60+
tag = "test"
5961
}`, dig1),
6062
Check: resource.ComposeAggregateTestCheckFunc(
6163
resource.TestCheckResourceAttr("oci_tag.test", "tagged_ref", want1),
6264
resource.TestCheckResourceAttr("oci_tag.test", "id", want1),
6365
),
6466
},
65-
},
66-
})
67-
68-
// The digest should be tagged with all three tags.
69-
for _, want := range []string{"foo", "bar", "baz"} {
70-
desc, err := remote.Get(repo.Tag(want))
71-
if err != nil {
72-
t.Errorf("failed to get image with tag %q: %v", want, err)
73-
}
74-
if desc.Digest != d1 {
75-
t.Errorf("image with tag %q has wrong digest: got %s, want %s", want, desc.Digest, d1)
76-
}
77-
}
78-
79-
// Point the tags to another digest.
80-
want2 := fmt.Sprintf("%s:foo@%s", repo, d2)
81-
resource.Test(t, resource.TestCase{
82-
PreCheck: func() { testAccPreCheck(t) },
83-
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
84-
Steps: []resource.TestStep{
67+
// Update and Read testing
8568
{
8669
Config: fmt.Sprintf(`resource "oci_tag" "test" {
87-
digest_ref = %q
88-
tags = ["foo", "bar", "baz"]
89-
}`, dig2),
70+
digest_ref = %q
71+
tag = "test2"
72+
}`, dig2),
9073
Check: resource.ComposeAggregateTestCheckFunc(
9174
resource.TestCheckResourceAttr("oci_tag.test", "tagged_ref", want2),
9275
resource.TestCheckResourceAttr("oci_tag.test", "id", want2),
9376
),
9477
},
78+
// Delete testing automatically occurs in TestCase
9579
},
9680
})
97-
98-
// The second digest should be tagged with all three tags.
99-
for _, want := range []string{"foo", "bar", "baz"} {
100-
desc, err := remote.Get(repo.Tag(want))
101-
if err != nil {
102-
t.Errorf("failed to get image with tag %q: %v", want, err)
103-
}
104-
if desc.Digest != d2 {
105-
t.Errorf("image with tag %q has wrong digest: got %s, want %s", want, desc.Digest, d2)
106-
}
107-
}
108-
109-
// Add a fourth tag.
110-
resource.Test(t, resource.TestCase{
111-
PreCheck: func() { testAccPreCheck(t) },
112-
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
113-
Steps: []resource.TestStep{
114-
{
115-
Config: fmt.Sprintf(`resource "oci_tag" "test" {
116-
digest_ref = %q
117-
tags = ["foo", "bar", "baz", "qux"]
118-
}`, dig2),
119-
Check: resource.ComposeAggregateTestCheckFunc(
120-
resource.TestCheckResourceAttr("oci_tag.test", "tagged_ref", want2),
121-
resource.TestCheckResourceAttr("oci_tag.test", "id", want2),
122-
),
123-
},
124-
},
125-
})
126-
127-
// The second digest should be tagged with all three tags.
128-
for _, want := range []string{"foo", "bar", "baz", "qux"} {
129-
desc, err := remote.Get(repo.Tag(want))
130-
if err != nil {
131-
t.Errorf("failed to get image with tag %q: %v", want, err)
132-
}
133-
if desc.Digest != d2 {
134-
t.Errorf("image with tag %q has wrong digest: got %s, want %s", want, desc.Digest, d2)
135-
}
136-
}
137-
138-
// Tag the digest with the same tag multiple times, which should be allowed but warn.
139-
resource.Test(t, resource.TestCase{
140-
PreCheck: func() { testAccPreCheck(t) },
141-
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
142-
Steps: []resource.TestStep{{
143-
Config: fmt.Sprintf(`resource "oci_tag" "test" {
144-
digest_ref = %q
145-
tags = ["foo", "foo", "foo", "bar", "bar", "bar"]
146-
}`, dig2),
147-
Check: resource.ComposeAggregateTestCheckFunc(
148-
resource.TestCheckResourceAttr("oci_tag.test", "tagged_ref", want2),
149-
resource.TestCheckResourceAttr("oci_tag.test", "id", want2),
150-
),
151-
}},
152-
})
15381
}

pkg/validators/tag_validator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// TagValidator is a string validator that checks that the string is valid OCI reference by digest.
1111
type TagValidator struct{}
1212

13-
var _ validator.String = TagValidator{}
13+
var _ validator.String = DigestValidator{}
1414

1515
func (v TagValidator) Description(context.Context) string {
1616
return `value must be a valid OCI tag element (e.g., "latest", "v1.2.3")`

0 commit comments

Comments
 (0)