Skip to content

Commit 6a31df0

Browse files
authored
feat(controlplane): make contract-names unique and DNS1123 compatible (#601)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 2b99e3b commit 6a31df0

19 files changed

+376
-135
lines changed

app/controlplane/internal/biz/biz.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -17,12 +17,13 @@ package biz
1717

1818
import (
1919
"crypto/rand"
20+
"errors"
2021
"fmt"
2122
"math/big"
2223
"strings"
2324

2425
"github.com/google/wire"
25-
"github.com/moby/moby/pkg/namesgenerator"
26+
"k8s.io/apimachinery/pkg/util/validation"
2627
)
2728

2829
// ProviderSet is biz providers.
@@ -53,16 +54,35 @@ var ProviderSet = wire.NewSet(
5354

5455
// generate a DNS1123-valid random name using moby's namesgenerator
5556
// plus an additional random number
56-
func generateRandomName() (string, error) {
57-
// Create a random name
58-
name := namesgenerator.GetRandomName(0)
59-
// and append a random number to it
57+
func generateValidDNS1123WithSuffix(prefix string) (string, error) {
58+
// Append a random number to it
6059
randomNumber, err := rand.Int(rand.Reader, big.NewInt(10000))
6160
if err != nil {
6261
return "", fmt.Errorf("failed to generate random number: %w", err)
6362
}
6463

6564
// Replace underscores with dashes to make it compatible with DNS1123
66-
name = strings.ReplaceAll(fmt.Sprintf("%s-%d", name, randomNumber), "_", "-")
65+
name := strings.ReplaceAll(fmt.Sprintf("%s-%d", prefix, randomNumber), "_", "-")
66+
67+
if err := ValidateIsDNS1123(name); err != nil {
68+
return "", fmt.Errorf("generated name is not DNS1123-valid: %w", err)
69+
}
70+
6771
return name, nil
6872
}
73+
74+
func ValidateIsDNS1123(name string) error {
75+
// The same validation done by Kubernetes for their namespace name
76+
// https://github.com/kubernetes/apimachinery/blob/fa98d6eaedb4caccd69fc07d90bbb6a1e551f00f/pkg/api/validation/generic.go#L63
77+
err := validation.IsDNS1123Label(name)
78+
if len(err) > 0 {
79+
errMsg := ""
80+
for _, e := range err {
81+
errMsg += e + "\n"
82+
}
83+
84+
return errors.New(errMsg)
85+
}
86+
87+
return nil
88+
}

app/controlplane/internal/biz/casmapping_integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,10 @@ func (s *casMappingIntegrationSuite) SetupTest() {
352352

353353
// Create workflowRun in the database
354354
// Workflow
355-
workflow, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test workflow", OrgID: s.org1.ID})
355+
workflow, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test-workflow", OrgID: s.org1.ID})
356356
assert.NoError(err)
357357

358-
publicWorkflow, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test workflow", OrgID: s.org1.ID, Public: true})
358+
publicWorkflow, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test-workflow", OrgID: s.org1.ID, Public: true})
359359
assert.NoError(err)
360360

361361
// Robot account

app/controlplane/internal/biz/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func (s *testSuite) SetupTest() {
193193
assert.NoError(err)
194194

195195
// Workflow
196-
s.workflow, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test workflow", OrgID: s.org.ID})
196+
s.workflow, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test-workflow", OrgID: s.org.ID})
197197
assert.NoError(err)
198198

199199
// Integration configuration

app/controlplane/internal/biz/organization.go

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323

2424
"github.com/go-kratos/kratos/v2/log"
2525
"github.com/google/uuid"
26-
"k8s.io/apimachinery/pkg/util/validation"
26+
"github.com/moby/moby/pkg/namesgenerator"
2727
)
2828

2929
type Organization struct {
@@ -55,7 +55,7 @@ func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iU
5555
}
5656
}
5757

58-
const OrganizationRandomNameMaxTries = 10
58+
const RandomNameMaxTries = 10
5959

6060
type createOptions struct {
6161
createInlineBackend bool
@@ -72,9 +72,10 @@ func WithCreateInlineBackend() CreateOpt {
7272

7373
func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context, opts ...CreateOpt) (*Organization, error) {
7474
// Try 10 times to create a random name
75-
for i := 0; i < OrganizationRandomNameMaxTries; i++ {
75+
for i := 0; i < RandomNameMaxTries; i++ {
7676
// Create a random name
77-
name, err := generateRandomName()
77+
prefix := namesgenerator.GetRandomName(0)
78+
name, err := generateValidDNS1123WithSuffix(prefix)
7879
if err != nil {
7980
return nil, fmt.Errorf("failed to generate random name: %w", err)
8081
}
@@ -115,7 +116,7 @@ var errOrgName = errors.New("org names must only contain lowercase letters, numb
115116
func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts ...CreateOpt) (*Organization, error) {
116117
uc.logger.Infow("msg", "Creating organization", "name", name)
117118

118-
if err := ValidateOrgName(name); err != nil {
119+
if err := ValidateIsDNS1123(name); err != nil {
119120
return nil, NewErrValidation(errOrgName)
120121
}
121122

@@ -139,22 +140,6 @@ func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts .
139140
return org, nil
140141
}
141142

142-
func ValidateOrgName(name string) error {
143-
// The same validation done by Kubernetes for their namespace name
144-
// https://github.com/kubernetes/apimachinery/blob/fa98d6eaedb4caccd69fc07d90bbb6a1e551f00f/pkg/api/validation/generic.go#L63
145-
err := validation.IsDNS1123Label(name)
146-
if len(err) > 0 {
147-
errMsg := ""
148-
for _, e := range err {
149-
errMsg += e + "\n"
150-
}
151-
152-
return errors.New(errMsg)
153-
}
154-
155-
return nil
156-
}
157-
158143
func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgID string, name *string) (*Organization, error) {
159144
userUUID, err := uuid.Parse(userID)
160145
if err != nil {
@@ -168,7 +153,7 @@ func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgID string,
168153

169154
// We validate the name to get ready for the name to become identifiers
170155
if name != nil {
171-
if err := ValidateOrgName(*name); err != nil {
156+
if err := ValidateIsDNS1123(*name); err != nil {
172157
return nil, NewErrValidation(errOrgName)
173158
}
174159
}
@@ -185,7 +170,7 @@ func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgID string,
185170
org, err := uc.orgRepo.Update(ctx, orgUUID, name)
186171
if err != nil {
187172
if errors.Is(err, ErrAlreadyExists) {
188-
return nil, NewErrValidationStr("an organization with that name already exists")
173+
return nil, NewErrValidationStr("a organization with that name already exists")
189174
}
190175

191176
return nil, fmt.Errorf("failed to update organization: %w", err)

app/controlplane/internal/biz/organization_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (s *OrgIntegrationTestSuite) SetupTest() {
248248
assert.NoError(err)
249249

250250
// Workflow + contract
251-
_, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test workflow", OrgID: s.org.ID})
251+
_, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test-workflow", OrgID: s.org.ID})
252252
assert.NoError(err)
253253

254254
// check integration, OCI repository and workflow and contracts are present in the db

app/controlplane/internal/biz/organization_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (s *organizationTestSuite) TestCreateWithRandomName() {
4949
s.Run("if it runs out of tries, it fails", func() {
5050
ctx := context.Background()
5151
// the first one fails because it already exists
52-
repo.On("Create", ctx, mock.Anything).Times(biz.OrganizationRandomNameMaxTries).Return(nil, biz.ErrAlreadyExists)
52+
repo.On("Create", ctx, mock.Anything).Times(biz.RandomNameMaxTries).Return(nil, biz.ErrAlreadyExists)
5353
got, err := uc.CreateWithRandomName(ctx)
5454
s.Error(err)
5555
s.Nil(got)
@@ -78,7 +78,7 @@ func (s *organizationTestSuite) TestValidateOrgName() {
7878

7979
for _, tc := range testCases {
8080
s.T().Run(tc.name, func(t *testing.T) {
81-
err := biz.ValidateOrgName(tc.name)
81+
err := biz.ValidateIsDNS1123(tc.name)
8282
if tc.expectedError {
8383
s.Error(err)
8484
} else {

app/controlplane/internal/biz/referrer_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ func (s *referrerIntegrationTestSuite) SetupTest() {
371371

372372
s.workflow1, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "wf", Team: "team", OrgID: s.org1.ID})
373373
require.NoError(s.T(), err)
374-
s.workflow2, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "wf from org 2", Team: "team", OrgID: s.org2.ID})
374+
s.workflow2, err = s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "wf-from-org-2", Team: "team", OrgID: s.org2.ID})
375375
require.NoError(s.T(), err)
376376

377377
// user 1 has access to org 1 and 2

app/controlplane/internal/biz/workflow.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,13 @@ func (uc *WorkflowUseCase) findOrCreateContract(ctx context.Context, orgID, cont
122122
}
123123

124124
// No contractID has been provided so we create a new one
125-
contractName := fmt.Sprintf("%s/%s", project, name)
126-
return uc.contractUC.Create(ctx, orgID, contractName, nil)
125+
contractName := name
126+
// Project might be empty
127+
if project != "" {
128+
contractName = fmt.Sprintf("%s-%s", project, name)
129+
}
130+
131+
return uc.contractUC.Create(ctx, &WorkflowContractCreateOpts{OrgID: orgID, Name: contractName, AddUniquePrefix: true})
127132
}
128133

129134
func (uc *WorkflowUseCase) List(ctx context.Context, orgID string) ([]*Workflow, error) {

app/controlplane/internal/biz/workflow_integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ func (s *workflowIntegrationTestSuite) TestContractLatestAvailable() {
5757
func (s *workflowIntegrationTestSuite) TestUpdate() {
5858
ctx := context.Background()
5959
const (
60-
name = "test workflow"
60+
name = "test-workflow"
6161
team = "test team"
62-
project = "test project"
62+
project = "test-project"
6363
description = "test description"
6464
)
6565

app/controlplane/internal/biz/workflowcontract.go

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -102,35 +102,94 @@ func (uc *WorkflowContractUseCase) FindByIDInOrg(ctx context.Context, orgID, con
102102
return uc.repo.FindByIDInOrg(ctx, orgUUID, contractUUID)
103103
}
104104

105+
type WorkflowContractCreateOpts struct {
106+
OrgID, Name string
107+
Schema *schemav1.CraftingSchema
108+
// Make sure that the name is unique in the organization
109+
AddUniquePrefix bool
110+
}
111+
105112
// we currently only support schema v1
106-
func (uc *WorkflowContractUseCase) Create(ctx context.Context, orgID, name string, schema *schemav1.CraftingSchema) (*WorkflowContract, error) {
107-
orgUUID, err := uuid.Parse(orgID)
113+
func (uc *WorkflowContractUseCase) Create(ctx context.Context, opts *WorkflowContractCreateOpts) (*WorkflowContract, error) {
114+
if opts.OrgID == "" || opts.Name == "" {
115+
return nil, NewErrValidationStr("organization and name are required")
116+
}
117+
118+
orgUUID, err := uuid.Parse(opts.OrgID)
108119
if err != nil {
109120
return nil, err
110121
}
111122

123+
if err := ValidateIsDNS1123(opts.Name); err != nil {
124+
return nil, NewErrValidation(err)
125+
}
126+
112127
// If no schema is provided we create an empty one
113-
if schema == nil {
114-
schema = &schemav1.CraftingSchema{
128+
if opts.Schema == nil {
129+
opts.Schema = &schemav1.CraftingSchema{
115130
SchemaVersion: "v1",
116131
}
132+
}
117133

118-
if err := schema.ValidateAll(); err != nil {
119-
return nil, err
120-
}
134+
if err := opts.Schema.ValidateAll(); err != nil {
135+
return nil, err
121136
}
122137

123-
rawSchema, err := proto.Marshal(schema)
138+
rawSchema, err := proto.Marshal(opts.Schema)
124139
if err != nil {
125140
return nil, err
126141
}
127142

128-
opts := &ContractCreateOpts{
129-
OrgID: orgUUID, Name: name,
143+
// Create a workflow with a unique name if needed
144+
args := &ContractCreateOpts{
145+
OrgID: orgUUID, Name: opts.Name,
130146
ContractBody: rawSchema,
131147
}
132148

133-
return uc.repo.Create(ctx, opts)
149+
var c *WorkflowContract
150+
if opts.AddUniquePrefix {
151+
c, err = uc.createWithUniqueName(ctx, args)
152+
} else {
153+
c, err = uc.repo.Create(ctx, args)
154+
}
155+
156+
if err != nil {
157+
if errors.Is(err, ErrAlreadyExists) {
158+
return nil, NewErrValidationStr("that name is already taken")
159+
}
160+
161+
return nil, fmt.Errorf("failed to create contract: %w", err)
162+
}
163+
164+
return c, nil
165+
}
166+
167+
func (uc *WorkflowContractUseCase) createWithUniqueName(ctx context.Context, opts *ContractCreateOpts) (*WorkflowContract, error) {
168+
originalName := opts.Name
169+
170+
for i := 0; i < RandomNameMaxTries; i++ {
171+
// append a suffix
172+
if i > 0 {
173+
var err error
174+
opts.Name, err = generateValidDNS1123WithSuffix(originalName)
175+
if err != nil {
176+
return nil, fmt.Errorf("failed to generate random name: %w", err)
177+
}
178+
}
179+
180+
c, err := uc.repo.Create(ctx, opts)
181+
if err != nil {
182+
if errors.Is(err, ErrAlreadyExists) {
183+
continue
184+
}
185+
186+
return nil, fmt.Errorf("failed to create contract: %w", err)
187+
}
188+
189+
return c, nil
190+
}
191+
192+
return nil, NewErrValidationStr("that name is already taken")
134193
}
135194

136195
func (uc *WorkflowContractUseCase) Describe(ctx context.Context, orgID, contractID string, revision int) (*WorkflowContractWithVersion, error) {
@@ -174,6 +233,10 @@ func (uc *WorkflowContractUseCase) Update(ctx context.Context, orgID, contractID
174233
return nil, err
175234
}
176235

236+
if err := ValidateIsDNS1123(name); err != nil {
237+
return nil, NewErrValidation(err)
238+
}
239+
177240
rawSchema, err := proto.Marshal(schema)
178241
if err != nil {
179242
return nil, err
@@ -182,11 +245,17 @@ func (uc *WorkflowContractUseCase) Update(ctx context.Context, orgID, contractID
182245
opts := &ContractUpdateOpts{OrgID: orgUUID, ContractID: contractUUID, ContractBody: rawSchema, Name: name}
183246

184247
c, err := uc.repo.Update(ctx, opts)
185-
if c == nil {
248+
if err != nil {
249+
if errors.Is(err, ErrAlreadyExists) {
250+
return nil, NewErrValidationStr("that name is already taken")
251+
}
252+
253+
return nil, fmt.Errorf("failed to update contract: %w", err)
254+
} else if c == nil {
186255
return nil, NewErrNotFound("contract")
187256
}
188257

189-
return c, err
258+
return c, nil
190259
}
191260

192261
// Delete soft-deletes the entry

0 commit comments

Comments
 (0)