Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions pkg/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ import (
)

var (
artifact string
replicas int32
dryRun bool
artifact string
replicas int32
dryRun bool
customName string
)

var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy application to Kubernetes",
Hidden: isExperimentalFlagNotSet,
RunE: func(_ *cobra.Command, _ []string) error {
name, err := getNameFromImageReference(artifact)
name, err := getSpinAppName(artifact, customName)
if err != nil {
return err
}
Expand Down Expand Up @@ -65,7 +66,7 @@ func init() {
deployCmd.Flags().BoolVar(&dryRun, "dry-run", false, "only print the kubernetes manifest without deploying")
deployCmd.Flags().Int32VarP(&replicas, "replicas", "r", 2, "Number of replicas for the application")
deployCmd.Flags().StringVarP(&artifact, "from", "f", "", "Reference in the registry of the application")

deployCmd.Flags().StringVarP(&customName, "name", "", "", "Overwrite the generated name of the application")
if err := deployCmd.MarkFlagRequired("from"); err != nil {
log.Fatal(err)
}
Expand Down
52 changes: 40 additions & 12 deletions pkg/cmd/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"os"
"regexp"
"strings"
"text/template"

Expand All @@ -14,6 +15,7 @@ import (
)

type ScaffoldOptions struct {
name string
autoscaler string
configfile string
cpuLimit string
Expand Down Expand Up @@ -250,8 +252,7 @@ func scaffold(opts ScaffoldOptions) ([]byte, error) {
if err := validateFlags(opts); err != nil {
return nil, err
}

name, err := getNameFromImageReference(opts.from)
name, err := getSpinAppName(opts.from, opts.name)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -302,18 +303,45 @@ func validateImageReference(imageRef string) bool {
return err == nil
}

func getNameFromImageReference(imageRef string) (string, error) {
ref, err := dockerparser.Parse(imageRef)
if err != nil {
return "", err
}
func validateName(name string) bool {
// ensure name is a valid DNS subdomain
const pattern = `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`

if strings.Contains(ref.ShortName(), "/") {
parts := strings.Split(ref.ShortName(), "/")
return parts[len(parts)-1], nil
// check length
if len(name) < 1 || len(name) > 253 {
return false
}

return ref.ShortName(), nil
// Compile the regex
re := regexp.MustCompile(pattern)

// Match the name against the pattern
return re.MatchString(name)
}

// / Retrieve the desired name for the SpinApp CR
// / If provided, custom name has highest priority
// / If not provided, the name is computed from the image reference
// / In either case, the resulting name will be validated using the `validateName` func
func getSpinAppName(imageRef string, customName string) (string, error) {
name := customName
if len(name) == 0 {
ref, err := dockerparser.Parse(imageRef)
if err != nil {
return "", err
}
switch strings.Contains(ref.ShortName(), "/") {
case true:
parts := strings.Split(ref.ShortName(), "/")
name = parts[len(parts)-1]
case false:
name = ref.ShortName()
}
}
if !validateName(name) {
return "", fmt.Errorf("invalid name provided. Must be a valid DNS subdomain name and not more than 253 chars")
}
return name, nil
}

func init() {
Expand All @@ -333,7 +361,7 @@ func init() {
scaffoldCmd.Flags().StringSliceVarP(&scaffoldOpts.imagePullSecrets, "image-pull-secret", "s", []string{}, "Secrets in the same namespace to use for pulling the image")
scaffoldCmd.PersistentFlags().StringToStringVarP(&scaffoldOpts.variables, "variable", "v", nil, "Application variable (name=value) to be provided to the application")
scaffoldCmd.PersistentFlags().StringSliceVarP(&scaffoldOpts.components, "component", "", nil, "Component ID to run. This can be specified multiple times. The default is all components.")

scaffoldCmd.Flags().StringVarP(&scaffoldOpts.name, "name", "", "", "Overwrite the generated name of the application")
if err := scaffoldCmd.MarkFlagRequired("from"); err != nil {
log.Fatal(err)
}
Expand Down
84 changes: 64 additions & 20 deletions pkg/cmd/scaffold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -111,6 +112,16 @@ func TestScaffoldOutput(t *testing.T) {
},
expected: "components.yml",
},
{
name: "overwrite name",
opts: ScaffoldOptions{
from: "ghcr.io/foo/example-app:v0.1.0",
replicas: 2,
executor: "containerd-shim-spin",
name: "my-custom-name",
},
expected: "overwrite_name.yml",
},
}

for _, tc := range testcases {
Expand Down Expand Up @@ -147,42 +158,59 @@ func TestValidateImageReference_ValidImageReference(t *testing.T) {
}
}

func TestGetNameFromImageReference(t *testing.T) {
func TestGetSpinAppName(t *testing.T) {
testCases := []struct {
reference string
name string
reference string
customName string
name string
}{
{
reference: "bacongobbler/hello-rust",
name: "hello-rust",
reference: "bacongobbler/hello-rust",
customName: "",
name: "hello-rust",
}, {
reference: "bacongobbler/hello-rust:v1.0.0",
name: "hello-rust",
reference: "bacongobbler/hello-rust:v1.0.0",
customName: "",
name: "hello-rust",
}, {

reference: "ghcr.io/bacongobbler/hello-rust",
name: "hello-rust",
reference: "ghcr.io/bacongobbler/hello-rust",
customName: "",
name: "hello-rust",
}, {
reference: "ghcr.io/bacongobbler/hello-rust:v1.0.0",
customName: "",
name: "hello-rust",
}, {
reference: "ghcr.io/spinkube/spinkube/runtime-class-manager:v1",
customName: "",
name: "runtime-class-manager",
}, {
reference: "ghcr.io/bacongobbler/hello-rust:v1.0.0",
name: "hello-rust",
reference: "nginx:latest",
customName: "",
name: "nginx",
}, {
reference: "ghcr.io/spinkube/spinkube/runtime-class-manager:v1",
name: "runtime-class-manager",
reference: "nginx:latest",
customName: "web-server",
name: "web-server",
}, {
reference: "nginx:latest",
name: "nginx",
reference: "nginx",
customName: "",
name: "nginx",
}, {
reference: "nginx",
name: "nginx",
reference: "ttl.sh/hello-spinkube@sha256:cc4b191d11728b4e9e024308f0c03aded893da2002403943adc9deb8c4ca1644",
customName: "",
name: "hello-spinkube",
}, {
reference: "ttl.sh/hello-spinkube@sha256:cc4b191d11728b4e9e024308f0c03aded893da2002403943adc9deb8c4ca1644",
name: "hello-spinkube",
reference: "ttl.sh/hello-spinkube@sha256:cc4b191d11728b4e9e024308f0c03aded893da2002403943adc9deb8c4ca1644",
customName: "hi-spinkube",
name: "hi-spinkube",
},
}

for _, tc := range testCases {
t.Run(tc.reference, func(t *testing.T) {
actualName, err := getNameFromImageReference(tc.reference)
actualName, err := getSpinAppName(tc.reference, tc.customName)
require.Nil(t, err)
require.Equal(t, tc.name, actualName, "Expected image name from reference")
})
Expand Down Expand Up @@ -305,6 +333,22 @@ func TestFlagValidation(t *testing.T) {
},
expectedError: "target memory utilization percentage (0) must be between 1 and 100",
},
{
name: "must provide valid DNS subdomain name",
opts: ScaffoldOptions{
from: "ghcr.io/foo/example-app:v0.1.0",
name: "my*app",
},
expectedError: "invalid name provided. Must be a valid DNS subdomain name and not more than 253 chars",
},
{
name: "must provide valid DNS subdomain name 2",
opts: ScaffoldOptions{
from: "ghcr.io/foo/example-app:v0.1.0",
name: strings.Repeat("a", 254),
},
expectedError: "invalid name provided. Must be a valid DNS subdomain name and not more than 253 chars",
},
}

for _, tc := range testcases {
Expand Down
8 changes: 8 additions & 0 deletions pkg/cmd/testdata/overwrite_name.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: core.spinkube.dev/v1alpha1
kind: SpinApp
metadata:
name: my-custom-name
spec:
image: "ghcr.io/foo/example-app:v0.1.0"
executor: containerd-shim-spin
replicas: 2