Skip to content

Commit 039c908

Browse files
ambermingxinjingxiang-z
authored andcommitted
feat: add validation for ng/cz input (#206)
Signed-off-by: Amber Xue <ambermingxin@nvidia.com>
1 parent c1afd92 commit 039c908

4 files changed

Lines changed: 167 additions & 48 deletions

File tree

cmd/fleetint/enroll.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io"
2222
"os"
23+
"regexp"
2324
"strings"
2425
"time"
2526

@@ -35,6 +36,9 @@ var (
3536
)
3637

3738
const defaultEnrollTimeout = time.Minute
39+
const reservedUnassignedName = "Unassigned"
40+
41+
var enrollMetadataNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9 ._-]*$`)
3842

3943
// resolveToken returns the SAK token from --token, --token-file, or stdin.
4044
func resolveToken(cliContext *cli.Context) (string, error) {
@@ -84,9 +88,20 @@ func resolveToken(cliContext *cli.Context) (string, error) {
8488
func enrollCommand(cliContext *cli.Context) error {
8589
baseEndpoint := cliContext.String("endpoint")
8690
force := cliContext.Bool("force")
91+
nodeGroup, err := validatedOptionalMetadataFlagValue(cliContext, "node-group", "Node group")
92+
if err != nil {
93+
return err
94+
}
95+
computeZone, err := validatedOptionalMetadataFlagValue(cliContext, "compute-zone", "Compute zone")
96+
if err != nil {
97+
return err
98+
}
99+
if err := validateReservedPairMetadata(nodeGroup, computeZone); err != nil {
100+
return err
101+
}
87102
metadata := &enrollment.EnrollMetadata{
88-
NodeGroup: optionalFlagValue(cliContext, "node-group"),
89-
ComputeZone: optionalFlagValue(cliContext, "compute-zone"),
103+
NodeGroup: nodeGroup,
104+
ComputeZone: computeZone,
90105
}
91106

92107
sakToken, err := resolveToken(cliContext)
@@ -124,10 +139,40 @@ func enrollCommand(cliContext *cli.Context) error {
124139
return performEnrollWorkflow(ctx, baseEndpoint, sakToken, cfg, metadata)
125140
}
126141

127-
func optionalFlagValue(cliContext *cli.Context, name string) *string {
142+
func validatedOptionalMetadataFlagValue(cliContext *cli.Context, name, fieldName string) (*string, error) {
128143
if !cliContext.IsSet(name) {
129-
return nil
144+
return nil, nil
130145
}
131146
value := strings.TrimSpace(cliContext.String(name))
132-
return &value
147+
// Explicit empty means clear/unassign for reserved assignment fields.
148+
if value == "" {
149+
return &value, nil
150+
}
151+
if strings.EqualFold(value, reservedUnassignedName) {
152+
return nil, fmt.Errorf("%s name %q is reserved; use empty value to clear assignment", fieldName, reservedUnassignedName)
153+
}
154+
if len(value) > 255 {
155+
return nil, fmt.Errorf("%s name must be 255 characters or fewer", fieldName)
156+
}
157+
if !enrollMetadataNamePattern.MatchString(value) {
158+
return nil, fmt.Errorf("%s name must start with a letter and contain only letters, numbers, spaces, hyphens, underscores, or periods", fieldName)
159+
}
160+
return &value, nil
161+
}
162+
163+
func validateReservedPairMetadata(nodeGroup, computeZone *string) error {
164+
nodeGroupSet := nodeGroup != nil
165+
computeZoneSet := computeZone != nil
166+
167+
if !nodeGroupSet && !computeZoneSet {
168+
return nil
169+
}
170+
if nodeGroupSet && computeZoneSet {
171+
nodeGroupEmpty := *nodeGroup == ""
172+
computeZoneEmpty := *computeZone == ""
173+
if nodeGroupEmpty == computeZoneEmpty {
174+
return nil
175+
}
176+
}
177+
return fmt.Errorf("--node-group and --compute-zone must be both omitted, both empty, or both non-empty")
133178
}

cmd/fleetint/enroll_test.go

Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ package main
1818
import (
1919
"bytes"
2020
"context"
21+
"flag"
2122
"fmt"
2223
"os"
2324
"path/filepath"
25+
"strings"
2426
"testing"
2527
"time"
2628

2729
"github.com/stretchr/testify/assert"
2830
"github.com/stretchr/testify/require"
31+
"github.com/urfave/cli"
2932

3033
"github.com/NVIDIA/fleet-intelligence-agent/internal/config"
3134
"github.com/NVIDIA/fleet-intelligence-agent/internal/enrollment"
@@ -288,42 +291,108 @@ func TestEnrollCommandPassesOptionalMetadata(t *testing.T) {
288291
require.NoError(t, err)
289292
}
290293

291-
func TestEnrollCommandTreatsExplicitEmptyMetadataAsClear(t *testing.T) {
292-
useMissingFleetintEnvFile(t)
294+
func TestValidatedOptionalMetadataFlagValueAllowsExplicitEmpty(t *testing.T) {
295+
flagSet := flag.NewFlagSet("enroll", flag.ContinueOnError)
296+
flagSet.String("node-group", "", "")
297+
require.NoError(t, flagSet.Set("node-group", ""))
298+
cliContext := cli.NewContext(cli.NewApp(), flagSet, nil)
293299

294-
originalRunPrecheck := runPrecheck
295-
originalEnrollWorkflow := performEnrollWorkflow
296-
t.Cleanup(func() {
297-
runPrecheck = originalRunPrecheck
298-
performEnrollWorkflow = originalEnrollWorkflow
300+
value, err := validatedOptionalMetadataFlagValue(cliContext, "node-group", "Node group")
301+
require.NoError(t, err)
302+
require.NotNil(t, value)
303+
require.Empty(t, *value)
304+
}
305+
306+
func TestValidatedOptionalMetadataFlagValueRejectsReservedUnassignedName(t *testing.T) {
307+
flagSet := flag.NewFlagSet("enroll", flag.ContinueOnError)
308+
flagSet.String("node-group", "", "")
309+
require.NoError(t, flagSet.Set("node-group", "unassigned"))
310+
cliContext := cli.NewContext(cli.NewApp(), flagSet, nil)
311+
312+
_, err := validatedOptionalMetadataFlagValue(cliContext, "node-group", "Node group")
313+
require.ErrorContains(t, err, `Node group name "Unassigned" is reserved; use empty value to clear assignment`)
314+
}
315+
316+
func TestEnrollCommandRejectsInvalidMetadataCharacters(t *testing.T) {
317+
app := App()
318+
app.Writer = &bytes.Buffer{}
319+
320+
err := app.Run([]string{
321+
"fleetint", "enroll",
322+
"--endpoint", "https://example.com",
323+
"--token", "token",
324+
"--compute-zone", "@bad-zone",
299325
})
326+
require.ErrorContains(t, err, "Compute zone name must start with a letter")
327+
}
300328

301-
runPrecheck = func() (precheck.Result, error) {
302-
return precheck.Result{
303-
Checks: []precheck.Check{
304-
{Name: "gpu-present", Message: "ok", Passed: true},
305-
},
306-
}, nil
329+
func TestEnrollCommandRejectsOverlongMetadataNames(t *testing.T) {
330+
app := App()
331+
app.Writer = &bytes.Buffer{}
332+
longName := strings.Repeat("a", 256)
333+
334+
err := app.Run([]string{
335+
"fleetint", "enroll",
336+
"--endpoint", "https://example.com",
337+
"--token", "token",
338+
"--node-group", longName,
339+
})
340+
require.ErrorContains(t, err, "Node group name must be 255 characters or fewer")
341+
}
342+
343+
func TestValidateReservedPairMetadata(t *testing.T) {
344+
strPtr := func(v string) *string { return &v }
345+
tests := []struct {
346+
name string
347+
nodeGroup *string
348+
computeZone *string
349+
wantErr bool
350+
}{
351+
{name: "both omitted", nodeGroup: nil, computeZone: nil, wantErr: false},
352+
{name: "both empty", nodeGroup: strPtr(""), computeZone: strPtr(""), wantErr: false},
353+
{name: "both non-empty", nodeGroup: strPtr("ng-a"), computeZone: strPtr("cz-a"), wantErr: false},
354+
{name: "node-group only", nodeGroup: strPtr("ng-a"), computeZone: nil, wantErr: true},
355+
{name: "compute-zone only", nodeGroup: nil, computeZone: strPtr("cz-a"), wantErr: true},
356+
{name: "node-group empty compute-zone non-empty", nodeGroup: strPtr(""), computeZone: strPtr("cz-a"), wantErr: true},
357+
{name: "node-group non-empty compute-zone empty", nodeGroup: strPtr("ng-a"), computeZone: strPtr(""), wantErr: true},
307358
}
308359

309-
performEnrollWorkflow = func(ctx context.Context, baseEndpoint, sakToken string, cfg *config.Config, metadata *enrollment.EnrollMetadata) error {
310-
require.NotNil(t, metadata)
311-
require.NotNil(t, metadata.NodeGroup)
312-
require.Empty(t, *metadata.NodeGroup)
313-
require.NotNil(t, metadata.ComputeZone)
314-
require.Empty(t, *metadata.ComputeZone)
315-
return nil
360+
for _, tc := range tests {
361+
t.Run(tc.name, func(t *testing.T) {
362+
err := validateReservedPairMetadata(tc.nodeGroup, tc.computeZone)
363+
if tc.wantErr {
364+
require.ErrorContains(t, err, "--node-group and --compute-zone must be both omitted, both empty, or both non-empty")
365+
return
366+
}
367+
require.NoError(t, err)
368+
})
316369
}
370+
}
317371

372+
func TestEnrollCommandRejectsMixedReservedPairValues(t *testing.T) {
318373
app := App()
319374
app.Writer = &bytes.Buffer{}
320375

321376
err := app.Run([]string{
322377
"fleetint", "enroll",
323378
"--endpoint", "https://example.com",
324379
"--token", "token",
325-
"--node-group=",
326-
"--compute-zone=",
380+
"--node-group", "",
381+
"--compute-zone", "cz-a",
327382
})
328-
require.NoError(t, err)
383+
require.ErrorContains(t, err, "--node-group and --compute-zone must be both omitted, both empty, or both non-empty")
384+
}
385+
386+
func TestEnrollCommandRejectsReservedUnassignedName(t *testing.T) {
387+
app := App()
388+
app.Writer = &bytes.Buffer{}
389+
390+
err := app.Run([]string{
391+
"fleetint", "enroll",
392+
"--endpoint", "https://example.com",
393+
"--token", "token",
394+
"--node-group", "Unassigned",
395+
"--compute-zone", "cz-a",
396+
})
397+
require.ErrorContains(t, err, `Node group name "Unassigned" is reserved; use empty value to clear assignment`)
329398
}

third_party/fleet-intelligence-sdk/pkg/metadata/metadata.go

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const (
2121
columnKey = "key"
2222
columnValue = "value"
2323
)
24+
const setMetadataUpsertQuery = `
25+
INSERT INTO gpud_metadata (key, value) VALUES (?, ?)
26+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2427

2528
// CreateTableMetadata creates the table for the metadata.
2629
func CreateTableMetadata(ctx context.Context, dbRW *sql.DB) error {
@@ -51,28 +54,10 @@ const (
5154

5255
// SetMetadata sets the value of a metadata entry.
5356
// If the metadata entry is not found, it is created.
54-
// If the metadata entry is found and the value is the same, it is not updated.
55-
// If the metadata entry is found and the value is different, it is updated.
57+
// If the metadata entry is found, it is updated in-place.
5658
func SetMetadata(ctx context.Context, dbRW *sql.DB, key string, value string) error {
57-
prev, err := ReadMetadata(ctx, dbRW, key)
58-
if err != nil {
59-
return err
60-
}
61-
62-
if prev == value {
63-
return nil
64-
}
65-
6659
start := time.Now()
67-
if prev == "" {
68-
// the "name" is not in the table, so we need to insert it
69-
_, err = dbRW.ExecContext(ctx, fmt.Sprintf(`
70-
INSERT INTO %s (%s, %s) VALUES (?, ?)`, tableNameGPUdMetadata, columnKey, columnValue), key, value)
71-
} else {
72-
// the "name" is already in the table, so we need to update it
73-
_, err = dbRW.ExecContext(ctx, fmt.Sprintf(`
74-
UPDATE %s SET %s = ? WHERE %s = ?`, tableNameGPUdMetadata, columnValue, columnKey), value, key)
75-
}
60+
_, err := dbRW.ExecContext(ctx, setMetadataUpsertQuery, key, value)
7661
pkgmetricsrecorder.RecordSQLiteInsertUpdate(time.Since(start).Seconds())
7762

7863
return err

third_party/fleet-intelligence-sdk/pkg/metadata/metadata_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,23 @@ func TestSetAndReadMetadata(t *testing.T) {
126126
_, err = ReadMetadata(canceledCtx, dbRO, testKey)
127127
assert.Error(t, err)
128128
}
129+
130+
func TestSetMetadata_AllowsTransitionFromEmptyToNonEmpty(t *testing.T) {
131+
t.Parallel()
132+
dbRW, dbRO, cleanup := sqlite.OpenTestDB(t)
133+
defer cleanup()
134+
135+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
136+
defer cancel()
137+
138+
require.NoError(t, CreateTableMetadata(ctx, dbRW))
139+
140+
const key = "nodegroup"
141+
require.NoError(t, SetMetadata(ctx, dbRW, key, "group-a"))
142+
require.NoError(t, SetMetadata(ctx, dbRW, key, ""))
143+
require.NoError(t, SetMetadata(ctx, dbRW, key, "group-b"))
144+
145+
value, err := ReadMetadata(ctx, dbRO, key)
146+
require.NoError(t, err)
147+
assert.Equal(t, "group-b", value)
148+
}

0 commit comments

Comments
 (0)