mirror of https://github.com/knative/func.git
feat: add support for labels in func.yaml (#373)
* feat: add support for labels in func.yaml and `func config` This change adds support for setting labels on deployed functions. It uses the interactive CLI prompt introduced by Zbynek to add, remove and list labels applied on a deployed function. Signed-off-by: Lance Ball <lball@redhat.com> * fixup: fix string output for Pair type Signed-off-by: Lance Ball <lball@redhat.com> * fixup: review feedback Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
parent
578b33856c
commit
0dba67751e
|
@ -21,8 +21,9 @@ var configCmd = &cobra.Command{
|
|||
Short: "Configure a function",
|
||||
Long: `Configure a function
|
||||
|
||||
Interactive propmt that allows configuration of Volume mounts and Environment variables for a function
|
||||
project present in the current directory or from the directory specified with --path.
|
||||
Interactive propmt that allows configuration of Volume mounts, Environment
|
||||
variables, and Labels for a function project present in the current directory
|
||||
or from the directory specified with --path.
|
||||
`,
|
||||
SuggestFor: []string{"cfg", "cofnig"},
|
||||
PreRunE: bindEnv("path"),
|
||||
|
@ -41,7 +42,7 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) {
|
|||
Name: "selectedConfig",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What do you want to configure?",
|
||||
Options: []string{"Environment values", "Volumes"},
|
||||
Options: []string{"Environment values", "Volumes", "Labels"},
|
||||
Default: "Environment values",
|
||||
},
|
||||
},
|
||||
|
@ -74,18 +75,24 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) {
|
|||
err = runAddVolumesPrompt(cmd.Context(), function)
|
||||
} else if answers.SelectedConfig == "Environment values" {
|
||||
err = runAddEnvsPrompt(cmd.Context(), function)
|
||||
} else if answers.SelectedConfig == "Labels" {
|
||||
err = runAddLabelsPrompt(cmd.Context(), function)
|
||||
}
|
||||
case "Remove":
|
||||
if answers.SelectedConfig == "Volumes" {
|
||||
err = runRemoveVolumesPrompt(function)
|
||||
} else if answers.SelectedConfig == "Environment values" {
|
||||
err = runRemoveEnvsPrompt(function)
|
||||
} else if answers.SelectedConfig == "Labels" {
|
||||
err = runRemoveLabelsPrompt(function)
|
||||
}
|
||||
case "List":
|
||||
if answers.SelectedConfig == "Volumes" {
|
||||
listVolumes(function)
|
||||
} else if answers.SelectedConfig == "Environment values" {
|
||||
listEnvs(function)
|
||||
} else if answers.SelectedConfig == "Labels" {
|
||||
listLabels(function)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
newEnv := fn.Env{}
|
||||
newEnv := fn.Pair{}
|
||||
|
||||
switch selectedOption {
|
||||
// SECTION - add new Environment variable with the specified value
|
||||
|
@ -403,7 +403,7 @@ func runRemoveEnvsPrompt(f fn.Function) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
var newEnvs fn.Envs
|
||||
var newEnvs fn.Pairs
|
||||
removed := false
|
||||
for i, e := range f.Envs {
|
||||
if e.String() == selectedEnv {
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configLabelsCmd)
|
||||
configLabelsCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||
configLabelsCmd.AddCommand(configLabelsAddCmd)
|
||||
configLabelsAddCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||
configLabelsCmd.AddCommand(configLabelsRemoveCmd)
|
||||
configLabelsRemoveCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||
}
|
||||
|
||||
var configLabelsCmd = &cobra.Command{
|
||||
Use: "labels",
|
||||
Short: "List and manage configured labels for a function",
|
||||
Long: `List and manage configured labels for a function
|
||||
|
||||
Prints configured labels for a function project present in
|
||||
the current directory or from the directory specified with --path.
|
||||
`,
|
||||
SuggestFor: []string{"albels", "abels", "label"},
|
||||
PreRunE: bindEnv("path"),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
function, err := initConfigCommand(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
listLabels(function)
|
||||
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
var configLabelsAddCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add labels to the function configuration",
|
||||
Long: `Add labels to the function configuration
|
||||
|
||||
Interactive prompt to add labels to the function project in the current
|
||||
directory or from the directory specified with --path.
|
||||
|
||||
The label can be set directly from a value or from an environment variable on
|
||||
the local machine.
|
||||
`,
|
||||
SuggestFor: []string{"ad", "create", "insert", "append"},
|
||||
PreRunE: bindEnv("path"),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
function, err := initConfigCommand(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return runAddLabelsPrompt(cmd.Context(), function)
|
||||
},
|
||||
}
|
||||
|
||||
var configLabelsRemoveCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove labels from the function configuration",
|
||||
Long: `Remove labels from the function configuration
|
||||
|
||||
Interactive prompt to remove labels from the function project in the current
|
||||
directory or from the directory specified with --path.
|
||||
`,
|
||||
SuggestFor: []string{"del", "delete", "rmeove"},
|
||||
PreRunE: bindEnv("path"),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
function, err := initConfigCommand(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return runRemoveLabelsPrompt(function)
|
||||
},
|
||||
}
|
||||
|
||||
func listLabels(f fn.Function) {
|
||||
if len(f.Labels) == 0 {
|
||||
fmt.Println("There aren't any configured labels")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Configured labels:")
|
||||
for _, e := range f.Labels {
|
||||
fmt.Println(" - ", e.String())
|
||||
}
|
||||
}
|
||||
|
||||
func runAddLabelsPrompt(ctx context.Context, f fn.Function) (err error) {
|
||||
|
||||
insertToIndex := 0
|
||||
|
||||
// SECTION - if there are some labels already set, choose the position of the new entry
|
||||
if len(f.Labels) > 0 {
|
||||
options := []string{}
|
||||
for _, e := range f.Labels {
|
||||
options = append(options, fmt.Sprintf("Insert before: %s", e.String()))
|
||||
}
|
||||
options = append(options, "Insert here.")
|
||||
|
||||
selectedLabel := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Where do you want to add the label?",
|
||||
Options: options,
|
||||
Default: options[len(options)-1],
|
||||
}
|
||||
err = survey.AskOne(prompt, &selectedLabel)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i, option := range options {
|
||||
if option == selectedLabel {
|
||||
insertToIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECTION - select the type of label to be added
|
||||
selectedOption := ""
|
||||
const (
|
||||
optionLabelValue = "Label with a specified value"
|
||||
optionLabelLocal = "Value from a local environment variable"
|
||||
)
|
||||
options := []string{optionLabelValue, optionLabelLocal}
|
||||
|
||||
err = survey.AskOne(&survey.Select{
|
||||
Message: "What type of label do you want to add?",
|
||||
Options: options,
|
||||
}, &selectedOption)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
newPair := fn.Pair{}
|
||||
|
||||
switch selectedOption {
|
||||
// SECTION - add new label with the specified value
|
||||
case optionLabelValue:
|
||||
qs := []*survey.Question{
|
||||
{
|
||||
Name: "name",
|
||||
Prompt: &survey.Input{Message: "Please specify the label name:"},
|
||||
Validate: func(val interface{}) error {
|
||||
return utils.ValidateLabelName(val.(string))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "value",
|
||||
Prompt: &survey.Input{Message: "Please specify the label value:"},
|
||||
Validate: func(val interface{}) error {
|
||||
return utils.ValidateLabelValue(val.(string))
|
||||
}},
|
||||
}
|
||||
answers := struct {
|
||||
Name string
|
||||
Value string
|
||||
}{}
|
||||
|
||||
err = survey.Ask(qs, &answers)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
newPair.Name = &answers.Name
|
||||
newPair.Value = &answers.Value
|
||||
|
||||
// SECTION - add new label with value from a local environment variable
|
||||
case optionLabelLocal:
|
||||
qs := []*survey.Question{
|
||||
{
|
||||
Name: "name",
|
||||
Prompt: &survey.Input{Message: "Please specify the label name:"},
|
||||
Validate: func(val interface{}) error {
|
||||
return utils.ValidateLabelName(val.(string))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "value",
|
||||
Prompt: &survey.Input{Message: "Please specify the local environment variable:"},
|
||||
Validate: func(val interface{}) error {
|
||||
return utils.ValidateLabelValue(val.(string))
|
||||
},
|
||||
},
|
||||
}
|
||||
answers := struct {
|
||||
Name string
|
||||
Value string
|
||||
}{}
|
||||
|
||||
err = survey.Ask(qs, &answers)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := os.LookupEnv(answers.Value); !ok {
|
||||
fmt.Printf("Warning: specified local environment variable %q is not set\n", answers.Value)
|
||||
}
|
||||
|
||||
value := fmt.Sprintf("{{ env:%s }}", answers.Value)
|
||||
newPair.Name = &answers.Name
|
||||
newPair.Value = &value
|
||||
}
|
||||
|
||||
// we have all necessary information -> let's insert the label to the selected position in the list
|
||||
if insertToIndex == len(f.Labels) {
|
||||
f.Labels = append(f.Labels, newPair)
|
||||
} else {
|
||||
f.Labels = append(f.Labels[:insertToIndex+1], f.Labels[insertToIndex:]...)
|
||||
f.Labels[insertToIndex] = newPair
|
||||
}
|
||||
|
||||
err = f.WriteConfig()
|
||||
if err == nil {
|
||||
fmt.Println("Label entry was added to the function configuration")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func runRemoveLabelsPrompt(f fn.Function) (err error) {
|
||||
if len(f.Labels) == 0 {
|
||||
fmt.Println("There aren't any configured labels")
|
||||
return
|
||||
}
|
||||
|
||||
options := []string{}
|
||||
for _, e := range f.Labels {
|
||||
options = append(options, e.String())
|
||||
}
|
||||
|
||||
selectedLabel := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Which labels do you want to remove?",
|
||||
Options: options,
|
||||
}
|
||||
err = survey.AskOne(prompt, &selectedLabel)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var newLabels fn.Pairs
|
||||
removed := false
|
||||
for i, e := range f.Labels {
|
||||
if e.String() == selectedLabel {
|
||||
newLabels = append(f.Labels[:i], f.Labels[i+1:]...)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if removed {
|
||||
f.Labels = newLabels
|
||||
err = f.WriteConfig()
|
||||
if err == nil {
|
||||
fmt.Println("Label was removed from the function configuration")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -62,6 +62,7 @@ builderMap:
|
|||
default: quay.io/boson/faas-go-builder
|
||||
envs: []
|
||||
annotations: {}
|
||||
labels: []
|
||||
`
|
||||
if err := ioutil.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -276,7 +276,7 @@ func envFromCmd(cmd *cobra.Command) (*util.OrderedMap, []string, error) {
|
|||
return util.NewOrderedMap(), []string{}, nil
|
||||
}
|
||||
|
||||
func mergeEnvs(envs fn.Envs, envToUpdate *util.OrderedMap, envToRemove []string) (fn.Envs, error) {
|
||||
func mergeEnvs(envs fn.Pairs, envToUpdate *util.OrderedMap, envToRemove []string) (fn.Pairs, error) {
|
||||
updated := sets.NewString()
|
||||
|
||||
for i := range envs {
|
||||
|
@ -294,7 +294,7 @@ func mergeEnvs(envs fn.Envs, envToUpdate *util.OrderedMap, envToRemove []string)
|
|||
if !updated.Has(name) {
|
||||
n := name
|
||||
v := value
|
||||
envs = append(envs, fn.Env{Name: &n, Value: &v})
|
||||
envs = append(envs, fn.Pair{Name: &n, Value: &v})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,7 +309,7 @@ func mergeEnvs(envs fn.Envs, envToUpdate *util.OrderedMap, envToRemove []string)
|
|||
|
||||
errMsg := fn.ValidateEnvs(envs)
|
||||
if len(errMsg) > 0 {
|
||||
return fn.Envs{}, fmt.Errorf(strings.Join(errMsg, "\n"))
|
||||
return fn.Pairs{}, fmt.Errorf(strings.Join(errMsg, "\n"))
|
||||
}
|
||||
|
||||
return envs, nil
|
||||
|
|
|
@ -18,77 +18,77 @@ func Test_mergeEnvMaps(t *testing.T) {
|
|||
v2 := "y"
|
||||
|
||||
type args struct {
|
||||
envs fn.Envs
|
||||
envs fn.Pairs
|
||||
toUpdate *util.OrderedMap
|
||||
toRemove []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want fn.Envs
|
||||
want fn.Pairs
|
||||
}{
|
||||
{
|
||||
"add new var to empty list",
|
||||
args{
|
||||
fn.Envs{},
|
||||
fn.Pairs{},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{a, v1}}),
|
||||
[]string{},
|
||||
},
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v1}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v1}},
|
||||
},
|
||||
{
|
||||
"add new var",
|
||||
args{
|
||||
fn.Envs{fn.Env{Name: &b, Value: &v2}},
|
||||
fn.Pairs{fn.Pair{Name: &b, Value: &v2}},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{a, v1}}),
|
||||
[]string{},
|
||||
},
|
||||
fn.Envs{fn.Env{Name: &b, Value: &v2}, fn.Env{Name: &a, Value: &v1}},
|
||||
fn.Pairs{fn.Pair{Name: &b, Value: &v2}, fn.Pair{Name: &a, Value: &v1}},
|
||||
},
|
||||
{
|
||||
"update var",
|
||||
args{
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v1}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v1}},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{a, v2}}),
|
||||
[]string{},
|
||||
},
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v2}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v2}},
|
||||
},
|
||||
{
|
||||
"update multiple vars",
|
||||
args{
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v1}, fn.Env{Name: &b, Value: &v2}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v1}, fn.Pair{Name: &b, Value: &v2}},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{a, v2}, {b, v1}}),
|
||||
[]string{},
|
||||
},
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v2}, fn.Env{Name: &b, Value: &v1}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v2}, fn.Pair{Name: &b, Value: &v1}},
|
||||
},
|
||||
{
|
||||
"remove var",
|
||||
args{
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v1}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v1}},
|
||||
util.NewOrderedMap(),
|
||||
[]string{a},
|
||||
},
|
||||
fn.Envs{},
|
||||
fn.Pairs{},
|
||||
},
|
||||
{
|
||||
"remove multiple vars",
|
||||
args{
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v1}, fn.Env{Name: &b, Value: &v2}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v1}, fn.Pair{Name: &b, Value: &v2}},
|
||||
util.NewOrderedMap(),
|
||||
[]string{a, b},
|
||||
},
|
||||
fn.Envs{},
|
||||
fn.Pairs{},
|
||||
},
|
||||
{
|
||||
"update and remove vars",
|
||||
args{
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v1}, fn.Env{Name: &b, Value: &v2}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v1}, fn.Pair{Name: &b, Value: &v2}},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{a, v2}}),
|
||||
[]string{b},
|
||||
},
|
||||
fn.Envs{fn.Env{Name: &a, Value: &v2}},
|
||||
fn.Pairs{fn.Pair{Name: &a, Value: &v2}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
76
config.go
76
config.go
|
@ -42,13 +42,13 @@ func (v Volume) String() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
type Envs []Env
|
||||
type Env struct {
|
||||
type Pairs []Pair
|
||||
type Pair struct {
|
||||
Name *string `yaml:"name,omitempty"`
|
||||
Value *string `yaml:"value"`
|
||||
}
|
||||
|
||||
func (e Env) String() string {
|
||||
func (e Pair) String() string {
|
||||
if e.Name == nil && e.Value != nil {
|
||||
match := regWholeSecret.FindStringSubmatch(*e.Value)
|
||||
if len(match) == 2 {
|
||||
|
@ -61,18 +61,18 @@ func (e Env) String() string {
|
|||
} else if e.Name != nil && e.Value != nil {
|
||||
match := regKeyFromSecret.FindStringSubmatch(*e.Value)
|
||||
if len(match) == 3 {
|
||||
return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from Secret \"%s\"", *e.Name, match[2], match[1])
|
||||
return fmt.Sprintf("Entry \"%s\" with value set from key \"%s\" from Secret \"%s\"", *e.Name, match[2], match[1])
|
||||
}
|
||||
match = regKeyFromConfigMap.FindStringSubmatch(*e.Value)
|
||||
if len(match) == 3 {
|
||||
return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from ConfigMap \"%s\"", *e.Name, match[2], match[1])
|
||||
return fmt.Sprintf("Entry \"%s\" with value set from key \"%s\" from ConfigMap \"%s\"", *e.Name, match[2], match[1])
|
||||
}
|
||||
match = regLocalEnv.FindStringSubmatch(*e.Value)
|
||||
if len(match) == 2 {
|
||||
return fmt.Sprintf("Env \"%s\" with value set from local env variable \"%s\"", *e.Name, match[1])
|
||||
return fmt.Sprintf("Entry \"%s\" with value set from local env variable \"%s\"", *e.Name, match[1])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Env \"%s\" with value \"%s\"", *e.Name, *e.Value)
|
||||
return fmt.Sprintf("Entry \"%s\" with value \"%s\"", *e.Name, *e.Value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -117,9 +117,10 @@ type config struct {
|
|||
Builder string `yaml:"builder"`
|
||||
BuilderMap map[string]string `yaml:"builderMap"`
|
||||
Volumes Volumes `yaml:"volumes"`
|
||||
Envs Envs `yaml:"envs"`
|
||||
Envs Pairs `yaml:"envs"`
|
||||
Annotations map[string]string `yaml:"annotations"`
|
||||
Options Options `yaml:"options"`
|
||||
Labels Pairs `yaml:"labels"`
|
||||
// Add new values to the toConfig/fromConfig functions.
|
||||
}
|
||||
|
||||
|
@ -166,7 +167,8 @@ func newConfig(root string) (c config, err error) {
|
|||
volumesErrors := validateVolumes(c.Volumes)
|
||||
envsErrors := ValidateEnvs(c.Envs)
|
||||
optionsErrors := validateOptions(c.Options)
|
||||
if len(volumesErrors) > 0 || len(envsErrors) > 0 || len(optionsErrors) > 0 {
|
||||
labelsErrors := ValidateLabels(c.Labels)
|
||||
if len(volumesErrors) > 0 || len(envsErrors) > 0 || len(optionsErrors) > 0 || len(labelsErrors) > 0 {
|
||||
// if there aren't any previously reported errors, we need to set the error message header first
|
||||
if errMsg == "" {
|
||||
errMsg = errMsgHeader
|
||||
|
@ -185,7 +187,9 @@ func newConfig(root string) (c config, err error) {
|
|||
for i := range optionsErrors {
|
||||
optionsErrors[i] = " " + optionsErrors[i]
|
||||
}
|
||||
|
||||
for i := range labelsErrors {
|
||||
labelsErrors[i] = " " + labelsErrors[i]
|
||||
}
|
||||
errMsg = errMsg + strings.Join(volumesErrors, "\n")
|
||||
// we have errors from both volumes and envs sections -> let's make sure they are both indented
|
||||
if len(volumesErrors) > 0 && len(envsErrors) > 0 {
|
||||
|
@ -197,6 +201,11 @@ func newConfig(root string) (c config, err error) {
|
|||
errMsg = errMsg + "\n"
|
||||
}
|
||||
errMsg = errMsg + strings.Join(optionsErrors, "\n")
|
||||
// now also handle labels related errors
|
||||
if len(labelsErrors) > 0 && (len(optionsErrors) > 0 || len(volumesErrors) > 0 || len(envsErrors) > 0) {
|
||||
errMsg = errMsg + "\n"
|
||||
}
|
||||
errMsg = errMsg + strings.Join(labelsErrors, "\n")
|
||||
}
|
||||
|
||||
if errMsg != "" {
|
||||
|
@ -221,6 +230,7 @@ func fromConfig(c config) (f Function) {
|
|||
Envs: c.Envs,
|
||||
Annotations: c.Annotations,
|
||||
Options: c.Options,
|
||||
Labels: c.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,6 +248,7 @@ func toConfig(f Function) config {
|
|||
Envs: f.Envs,
|
||||
Annotations: f.Annotations,
|
||||
Options: f.Options,
|
||||
Labels: f.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,7 +307,7 @@ func validateVolumes(volumes Volumes) (errors []string) {
|
|||
// - name: EXAMPLE4
|
||||
// value: {{ configMap:configMapName:key }} # ENV from a key in configMap
|
||||
// - value: {{ configMap:configMapName }} # all key-pair values from configMap are set as ENV
|
||||
func ValidateEnvs(envs Envs) (errors []string) {
|
||||
func ValidateEnvs(envs Pairs) (errors []string) {
|
||||
|
||||
for i, env := range envs {
|
||||
if env.Name == nil && env.Value == nil {
|
||||
|
@ -332,6 +343,49 @@ func ValidateEnvs(envs Envs) (errors []string) {
|
|||
return
|
||||
}
|
||||
|
||||
// ValidateLabels checks that input labels are correct and contain all necessary fields.
|
||||
// Returns array of error messages, empty if no errors are found
|
||||
//
|
||||
// Allowed settings:
|
||||
// - name: EXAMPLE1 # label directly from a value
|
||||
// value: value1
|
||||
// - name: EXAMPLE2 # label from the local ENV var
|
||||
// value: {{ env:MY_ENV }}
|
||||
func ValidateLabels(labels Pairs) (errors []string) {
|
||||
for i, label := range labels {
|
||||
if label.Name == nil && label.Value == nil {
|
||||
errors = append(errors, fmt.Sprintf("label entry #%d is not properly set", i))
|
||||
} else if label.Value == nil {
|
||||
errors = append(errors, fmt.Sprintf("label entry #%d is missing value field, only name '%s' is set", i, *label.Name))
|
||||
} else {
|
||||
|
||||
if err := utils.ValidateLabelName(*label.Name); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("label entry #%d has invalid name or value set: %q %q; %s", i, *label.Name, *label.Value, err.Error()))
|
||||
} else if err := utils.ValidateLabelValue(*label.Value); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("label entry #%d has invalid name or value set: %q %q; %s", i, *label.Name, *label.Value, err.Error()))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(*label.Value, "{{") {
|
||||
// ENV from the local ENV var; {{ env:MY_ENV }}
|
||||
if !regLocalEnv.MatchString(*label.Value) {
|
||||
errors = append(errors,
|
||||
fmt.Sprintf(
|
||||
"label entry #%d with name '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}'",
|
||||
i, *label.Name, *label.Value))
|
||||
} else {
|
||||
match := regLocalEnv.FindStringSubmatch(*label.Value)
|
||||
value := os.Getenv(match[1])
|
||||
if err := utils.ValidateLabelValue(value); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("label entry #%d with name '%s' has invalid value when the environment is evaluated: '%s': %s", i, *label.Name, value, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// validateOptions checks that input Options are correctly set.
|
||||
// Returns array of error messages, empty if no errors are found
|
||||
func validateOptions(options Options) (errors []string) {
|
||||
|
|
387
config_test.go
387
config_test.go
|
@ -3,6 +3,7 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"knative.dev/pkg/ptr"
|
||||
|
@ -179,13 +180,13 @@ func Test_validateEnvs(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
envs Envs
|
||||
envs Pairs
|
||||
errs int
|
||||
}{
|
||||
{
|
||||
"correct entry - single env with value",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &value,
|
||||
},
|
||||
|
@ -194,8 +195,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - missing value",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
},
|
||||
},
|
||||
|
@ -203,8 +204,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - invalid name",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &incorrectName,
|
||||
Value: &value,
|
||||
},
|
||||
|
@ -213,8 +214,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - invalid name2",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &incorrectName2,
|
||||
Value: &value,
|
||||
},
|
||||
|
@ -223,12 +224,12 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - multiple envs with value",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &value,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name2,
|
||||
Value: &value2,
|
||||
},
|
||||
|
@ -237,11 +238,11 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - mmissing value - multiple envs",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name2,
|
||||
},
|
||||
},
|
||||
|
@ -249,8 +250,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - single env with value Local env",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
|
@ -259,16 +260,16 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - multiple envs with value Local env",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv3,
|
||||
},
|
||||
|
@ -277,20 +278,20 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - multiple envs with value Local env",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect3,
|
||||
},
|
||||
|
@ -299,8 +300,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - single secret with key",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey,
|
||||
},
|
||||
|
@ -309,8 +310,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - single configMap with key",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueConfigMapKey,
|
||||
},
|
||||
|
@ -319,16 +320,16 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - multiple secrets with key",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey3,
|
||||
},
|
||||
|
@ -337,12 +338,12 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - both secret and configmap with key",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueConfigMapKey,
|
||||
},
|
||||
|
@ -351,8 +352,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - single secret with key",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKeyIncorrect,
|
||||
},
|
||||
|
@ -361,20 +362,20 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - mutliple secrets with key",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKeyIncorrect,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKeyIncorrect2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKeyIncorrect3,
|
||||
},
|
||||
|
@ -383,8 +384,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - single whole secret",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Value: &valueSecret,
|
||||
},
|
||||
},
|
||||
|
@ -392,8 +393,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - single whole configMap",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Value: &valueConfigMap,
|
||||
},
|
||||
},
|
||||
|
@ -401,14 +402,14 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - multiple whole secret",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Value: &valueSecret,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecret2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecret3,
|
||||
},
|
||||
},
|
||||
|
@ -416,11 +417,11 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - both whole secret and configMap",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Value: &valueSecret,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueConfigMap,
|
||||
},
|
||||
},
|
||||
|
@ -428,8 +429,8 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - single whole secret",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Value: &value,
|
||||
},
|
||||
},
|
||||
|
@ -437,29 +438,29 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"incorrect entry - multiple whole secret",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Value: &valueSecretIncorrect,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecretIncorrect2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecretIncorrect3,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &value,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueLocalEnv2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueLocalEnv3,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecret,
|
||||
},
|
||||
},
|
||||
|
@ -467,52 +468,52 @@ func Test_validateEnvs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"correct entry - all combinations",
|
||||
Envs{
|
||||
Env{
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &value,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name2,
|
||||
Value: &value2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv3,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecret,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecret2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueSecret3,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Value: &valueConfigMap,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey2,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueSecretKey3,
|
||||
},
|
||||
Env{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueConfigMapKey,
|
||||
},
|
||||
|
@ -531,6 +532,232 @@ func Test_validateEnvs(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func Test_validateLabels(t *testing.T) {
|
||||
|
||||
name := "name"
|
||||
name2 := "name-two"
|
||||
name3 := "prefix.io/name3"
|
||||
value := "value"
|
||||
value2 := "value2"
|
||||
value3 := "value3"
|
||||
|
||||
incorrectName := ",foo"
|
||||
incorrectName2 := ":foo"
|
||||
incorrectValue := ":foo"
|
||||
|
||||
valueLocalEnv := "{{ env:MY_ENV }}"
|
||||
valueLocalEnv2 := "{{ env:MY_ENV2 }}"
|
||||
valueLocalEnv3 := "{{env:MY_ENV3}}"
|
||||
valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
|
||||
valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
|
||||
valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
|
||||
|
||||
os.Setenv("BAD_EXAMPLE", ":invalid")
|
||||
valueLocalEnvIncorrect4 := "{{env:BAD_EXAMPLE}}"
|
||||
|
||||
os.Setenv("GOOD_EXAMPLE", "valid")
|
||||
valueLocalEnv4 := "{{env:GOOD_EXAMPLE}}"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
labels Pairs
|
||||
errs int
|
||||
}{
|
||||
{
|
||||
"correct entry - single label with value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &value,
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
"correct entry - prefixed label with value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name3,
|
||||
Value: &value3,
|
||||
},
|
||||
},
|
||||
0,
|
||||
}, {
|
||||
"incorrect entry - missing value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
},
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
"incorrect entry - invalid name",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &incorrectName,
|
||||
Value: &value,
|
||||
},
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
"incorrect entry - invalid name2",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &incorrectName2,
|
||||
Value: &value,
|
||||
},
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
"incorrect entry - invalid value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &incorrectValue,
|
||||
},
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
"correct entry - multiple labels with value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &value,
|
||||
},
|
||||
Pair{
|
||||
Name: &name2,
|
||||
Value: &value2,
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
"incorrect entry - missing value - multiple labels",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
},
|
||||
Pair{
|
||||
Name: &name2,
|
||||
},
|
||||
},
|
||||
2,
|
||||
},
|
||||
{
|
||||
"correct entry - single label with value from local env",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
"correct entry - multiple labels with values from Local env",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv2,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv3,
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
"incorrect entry - multiple labels with values from Local env",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect2,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect3,
|
||||
},
|
||||
},
|
||||
3,
|
||||
},
|
||||
{
|
||||
"correct entry - good environment variable value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv4,
|
||||
},
|
||||
},
|
||||
0,
|
||||
}, {
|
||||
"incorrect entry - bad environment variable value",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnvIncorrect4,
|
||||
},
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
"correct entry - all combinations",
|
||||
Pairs{
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &value,
|
||||
},
|
||||
Pair{
|
||||
Name: &name2,
|
||||
Value: &value2,
|
||||
},
|
||||
Pair{
|
||||
Name: &name3,
|
||||
Value: &value3,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv2,
|
||||
},
|
||||
Pair{
|
||||
Name: &name,
|
||||
Value: &valueLocalEnv3,
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ValidateLabels(tt.labels); len(got) != tt.errs {
|
||||
t.Errorf("validateLabels() = %v\n got %d errors but want %d", got, len(got), tt.errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_validateOptions(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
|
|
|
@ -51,17 +51,40 @@ envs:
|
|||
- value: '{{ configMap:myconfigmap2 }}' # (4) all key-value pairs in ConfigMap as env variables
|
||||
```
|
||||
|
||||
### `volumes`
|
||||
Kubernetes Secrets or ConfigMaps can be mounted to the function as a Kubernetes Volume accessible under specified path. Below you can see an example how to mount the Secret `mysecret` to the path `/workspace/secret` and the ConfigMap `myconfigmap` to the path `/workspace/configmap`. This Secret/ConfigMap needs to be created before it is referenced in a function.
|
||||
### `image`
|
||||
|
||||
This is the image name for your function after it has been built. This field
|
||||
may be modified and `func` will create your image with the new name the next
|
||||
time you run `kn func build` or `kn func deploy`.
|
||||
|
||||
### `imageDigest`
|
||||
|
||||
This is the `sha256` hash of the image manifest when it is deployed. This value
|
||||
should not be modified.
|
||||
|
||||
### `labels`
|
||||
|
||||
The `labels` field allows you to set labels on a deployed function. Labels can be set
|
||||
directly from a value or from a local environment value. Eg. `'{{ env:USER }}'`, for more details see [Local Environment Variables section](#local-environment-variables).
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- secret: mysecret
|
||||
path: /workspace/secret
|
||||
- configMap: myconfigmap
|
||||
path: /workspace/configmap
|
||||
labels:
|
||||
- name: role # (1) label directly from a value
|
||||
value: backend
|
||||
- name: author # (2) label from a local environment value
|
||||
value: '{{ env:USER }}'
|
||||
```
|
||||
|
||||
### `name`
|
||||
|
||||
The name of your function. This value will be used as the name for your service
|
||||
when it is deployed. This value may be changed to rename the function on
|
||||
subsequent deployments.
|
||||
|
||||
### `namespace`
|
||||
|
||||
The Kubernetes namespace where your function will be deployed.
|
||||
|
||||
### `options`
|
||||
Options allows you to set specific configuration for the deployed function, allowing you to tweak Knative Service options related to autoscaling and other properties. If these options are not set, the Knative defaults will be used.
|
||||
- `scale`
|
||||
|
@ -97,27 +120,6 @@ options:
|
|||
concurrency: 100
|
||||
```
|
||||
|
||||
### `image`
|
||||
|
||||
This is the image name for your function after it has been built. This field
|
||||
may be modified and `func` will create your image with the new name the next
|
||||
time you run `kn func build` or `kn func deploy`.
|
||||
|
||||
### `imageDigest`
|
||||
|
||||
This is the `sha256` hash of the image manifest when it is deployed. This value
|
||||
should not be modified.
|
||||
|
||||
### `name`
|
||||
|
||||
The name of your function. This value will be used as the name for your service
|
||||
when it is deployed. This value may be changed to rename the function on
|
||||
subsequent deployments.
|
||||
|
||||
### `namespace`
|
||||
|
||||
The Kubernetes namespace where your function will be deployed.
|
||||
|
||||
### `runtime`
|
||||
|
||||
The language runtime for your function. For example `python`.
|
||||
|
@ -128,6 +130,18 @@ The source code template tailored for the invocation event that triggers
|
|||
your function. For example `http` for plain HTTP requests, `event` for
|
||||
CloudEvent triggered functions.
|
||||
|
||||
### `volumes`
|
||||
Kubernetes Secrets or ConfigMaps can be mounted to the function as a Kubernetes Volume accessible under specified path. Below you can see an example how to mount the Secret `mysecret` to the path `/workspace/secret` and the ConfigMap `myconfigmap` to the path `/workspace/configmap`. This Secret/ConfigMap needs to be created before it is referenced in a function.
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- secret: mysecret
|
||||
path: /workspace/secret
|
||||
- configMap: myconfigmap
|
||||
path: /workspace/configmap
|
||||
```
|
||||
|
||||
|
||||
## Local Environment Variables
|
||||
|
||||
Any of the fields in `func.yaml` may contain a reference to an environment
|
||||
|
|
|
@ -56,7 +56,7 @@ type Function struct {
|
|||
Volumes Volumes
|
||||
|
||||
// Env variables to be set
|
||||
Envs Envs
|
||||
Envs Pairs
|
||||
|
||||
// Map containing user-supplied annotations
|
||||
// Example: { "division": "finance" }
|
||||
|
@ -64,6 +64,9 @@ type Function struct {
|
|||
|
||||
// Options to be set on deployed function (scaling, etc.)
|
||||
Options Options
|
||||
|
||||
// Map of user-supplied labels
|
||||
Labels Pairs
|
||||
}
|
||||
|
||||
// NewFunction loads a Function from a path on disk. use .Initialized() to determine if
|
||||
|
|
|
@ -56,7 +56,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (result fn.Deploym
|
|||
referencedSecrets := sets.NewString()
|
||||
referencedConfigMaps := sets.NewString()
|
||||
|
||||
service, err := generateNewService(f.Name, f.ImageWithDigest(), f.Runtime, f.Envs, f.Volumes, f.Annotations, f.Options)
|
||||
service, err := generateNewService(f)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("knative deployer failed to generate the Knative Service: %v", err)
|
||||
return fn.DeploymentResult{}, err
|
||||
|
@ -120,7 +120,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (result fn.Deploym
|
|||
return fn.DeploymentResult{}, err
|
||||
}
|
||||
|
||||
_, err = client.UpdateServiceWithRetry(ctx, f.Name, updateService(f.ImageWithDigest(), newEnv, newEnvFrom, newVolumes, newVolumeMounts, f.Annotations, f.Options), 3)
|
||||
_, err = client.UpdateServiceWithRetry(ctx, f.Name, updateService(f, newEnv, newEnvFrom, newVolumes, newVolumeMounts), 3)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("knative deployer failed to update the Knative Service: %v", err)
|
||||
return fn.DeploymentResult{}, err
|
||||
|
@ -149,14 +149,14 @@ func probeFor(url string) *corev1.Probe {
|
|||
}
|
||||
}
|
||||
|
||||
func generateNewService(name, image, runtime string, envs fn.Envs, volumes fn.Volumes, annotations map[string]string, options fn.Options) (*servingv1.Service, error) {
|
||||
func generateNewService(f fn.Function) (*servingv1.Service, error) {
|
||||
containers := []corev1.Container{
|
||||
{
|
||||
Image: image,
|
||||
Image: f.ImageWithDigest(),
|
||||
},
|
||||
}
|
||||
|
||||
if runtime != "quarkus" {
|
||||
if f.Runtime != "quarkus" {
|
||||
containers[0].LivenessProbe = probeFor("/health/liveness")
|
||||
containers[0].ReadinessProbe = probeFor("/health/readiness")
|
||||
}
|
||||
|
@ -164,27 +164,29 @@ func generateNewService(name, image, runtime string, envs fn.Envs, volumes fn.Vo
|
|||
referencedSecrets := sets.NewString()
|
||||
referencedConfigMaps := sets.NewString()
|
||||
|
||||
newEnv, newEnvFrom, err := processEnvs(envs, &referencedSecrets, &referencedConfigMaps)
|
||||
newEnv, newEnvFrom, err := processEnvs(f.Envs, &referencedSecrets, &referencedConfigMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containers[0].Env = newEnv
|
||||
containers[0].EnvFrom = newEnvFrom
|
||||
|
||||
newVolumes, newVolumeMounts, err := processVolumes(volumes, &referencedSecrets, &referencedConfigMaps)
|
||||
newVolumes, newVolumeMounts, err := processVolumes(f.Volumes, &referencedSecrets, &referencedConfigMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containers[0].VolumeMounts = newVolumeMounts
|
||||
|
||||
labels, err := processLabels(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
"boson.dev/function": "true",
|
||||
"boson.dev/runtime": runtime,
|
||||
},
|
||||
Annotations: annotations,
|
||||
Name: f.Name,
|
||||
Labels: labels,
|
||||
Annotations: f.Annotations,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
ConfigurationSpec: v1.ConfigurationSpec{
|
||||
|
@ -200,7 +202,7 @@ func generateNewService(name, image, runtime string, envs fn.Envs, volumes fn.Vo
|
|||
},
|
||||
}
|
||||
|
||||
err = setServiceOptions(&service.Spec.Template, options)
|
||||
err = setServiceOptions(&service.Spec.Template, f.Options)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
@ -208,25 +210,30 @@ func generateNewService(name, image, runtime string, envs fn.Envs, volumes fn.Vo
|
|||
return service, nil
|
||||
}
|
||||
|
||||
func updateService(image string, newEnv []corev1.EnvVar, newEnvFrom []corev1.EnvFromSource, newVolumes []corev1.Volume, newVolumeMounts []corev1.VolumeMount,
|
||||
annotations map[string]string, options fn.Options) func(service *servingv1.Service) (*servingv1.Service, error) {
|
||||
func updateService(f fn.Function, newEnv []corev1.EnvVar, newEnvFrom []corev1.EnvFromSource, newVolumes []corev1.Volume, newVolumeMounts []corev1.VolumeMount) func(service *servingv1.Service) (*servingv1.Service, error) {
|
||||
return func(service *servingv1.Service) (*servingv1.Service, error) {
|
||||
// Removing the name so the k8s server can fill it in with generated name,
|
||||
// this prevents conflicts in Revision name when updating the KService from multiple places.
|
||||
service.Spec.Template.Name = ""
|
||||
|
||||
// Don't bother being as clever as we are with env variables
|
||||
// Just set the annotations to be whatever we find in func.yaml
|
||||
for k, v := range annotations {
|
||||
// Just set the annotations and labels to be whatever we find in func.yaml
|
||||
for k, v := range f.Annotations {
|
||||
service.ObjectMeta.Annotations[k] = v
|
||||
}
|
||||
|
||||
err := setServiceOptions(&service.Spec.Template, options)
|
||||
err := setServiceOptions(&service.Spec.Template, f.Options)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
err = flags.UpdateImage(&service.Spec.Template.Spec.PodSpec, image)
|
||||
labels, err := processLabels(f)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
service.ObjectMeta.Labels = labels
|
||||
|
||||
err = flags.UpdateImage(&service.Spec.Template.Spec.PodSpec, f.ImageWithDigest())
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
@ -241,6 +248,41 @@ func updateService(image string, newEnv []corev1.EnvVar, newEnvFrom []corev1.Env
|
|||
}
|
||||
}
|
||||
|
||||
// processLabels generates a map of labels as key/value pairs from a function config
|
||||
// labels:
|
||||
// - name: EXAMPLE1 # Label directly from a value
|
||||
// value: value1
|
||||
// - name: EXAMPLE2 # Label from the local ENV var
|
||||
// value: {{ env:MY_ENV }}
|
||||
func processLabels(f fn.Function) (map[string]string, error) {
|
||||
labels := map[string]string{
|
||||
"boson.dev/function": "true",
|
||||
"boson.dev/runtime": f.Runtime,
|
||||
}
|
||||
for _, label := range f.Labels {
|
||||
if label.Name != nil && label.Value != nil {
|
||||
if strings.HasPrefix(*label.Value, "{{") {
|
||||
slices := strings.Split(strings.Trim(*label.Value, "{} "), ":")
|
||||
if len(slices) == 2 {
|
||||
// label from the local ENV var, eg. author={{ env:$USER }}
|
||||
localValue, err := processLocalEnvValue(*label.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labels[*label.Name] = localValue
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// a standard label with key and value, eg. author=alice@example.com
|
||||
labels[*label.Name] = *label.Value
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// processEnvs generates array of EnvVars and EnvFromSources from a function config
|
||||
// envs:
|
||||
// - name: EXAMPLE1 # ENV directly from a value
|
||||
|
@ -253,7 +295,7 @@ func updateService(image string, newEnv []corev1.EnvVar, newEnvFrom []corev1.Env
|
|||
// - name: EXAMPLE4
|
||||
// value: {{ configMap:configMapName:key }} # ENV from a key in ConfigMap
|
||||
// - value: {{ configMap:configMapName }} # all key-pair values from ConfigMap are set as ENV
|
||||
func processEnvs(envs fn.Envs, referencedSecrets, referencedConfigMaps *sets.String) ([]corev1.EnvVar, []corev1.EnvFromSource, error) {
|
||||
func processEnvs(envs fn.Pairs, referencedSecrets, referencedConfigMaps *sets.String) ([]corev1.EnvVar, []corev1.EnvFromSource, error) {
|
||||
|
||||
envVars := []corev1.EnvVar{{Name: "BUILT", Value: time.Now().Format("20060102T150405")}}
|
||||
envFrom := []corev1.EnvFromSource{}
|
||||
|
|
|
@ -13,6 +13,9 @@ type ErrInvalidFunctionName error
|
|||
// ErrInvalidEnvVarName indicates the name did not pass env var name validation.
|
||||
type ErrInvalidEnvVarName error
|
||||
|
||||
// ErrInvalidLabel indicates the name did not pass label key validation, or the value did not pass label value validation.
|
||||
type ErrInvalidLabel error
|
||||
|
||||
// ValidateFunctionName validatest that the input name is a valid function name, ie. valid DNS-1123 label.
|
||||
// It must consist of lower case alphanumeric characters or '-' and start and end with an alphanumeric character
|
||||
// (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')
|
||||
|
@ -40,3 +43,35 @@ func ValidateEnvVarName(name string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateLabelName validates that the input name is a valid Kubernetes key.
|
||||
// Valid label names have two segments: an optional prefix and name, separated by a slash (/).
|
||||
// The name segment is required and must be 63 characters or less, beginning and ending with
|
||||
// an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and
|
||||
// alphanumerics between. The prefix is optional. If specified, the prefix must be a DNS subdomain:
|
||||
// a series of DNS labels separated by dots (.), not longer than 253 characters in total, followed
|
||||
// by a slash (/).
|
||||
func ValidateLabelName(name string) error {
|
||||
errs := validation.IsQualifiedName(name)
|
||||
if len(errs) > 0 {
|
||||
return ErrInvalidLabel(errors.New(strings.Join(errs, "")))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateLabelValue ensures that the input is a Kubernetes label value
|
||||
// Valid label values must be 63 characters or less (can be empty),
|
||||
// unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]),
|
||||
// could contain dashes (-), underscores (_), dots (.), and alphanumerics between.
|
||||
// Label values may also come from the environment and therefore, could be enclosed with {{}}
|
||||
// Treat this as a special case.
|
||||
func ValidateLabelValue(value string) error {
|
||||
var errs []string
|
||||
if !strings.HasPrefix(value, "{{") {
|
||||
errs = append(errs, validation.IsValidLabelValue(value)...)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return ErrInvalidLabel(errors.New(strings.Join(errs, "")))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestValidateFunctionName tests that only correct function names are accepted
|
||||
func TestValidateFunctionName(t *testing.T) {
|
||||
|
@ -62,3 +64,66 @@ func TestValidateEnvVarName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLabelName(t *testing.T) {
|
||||
cases := []struct {
|
||||
In string
|
||||
Valid bool
|
||||
}{
|
||||
{"", false},
|
||||
{"*", false},
|
||||
{"example", true},
|
||||
{"example-com", true},
|
||||
{"example.com", true},
|
||||
{"-example-com", false},
|
||||
{"example-com-", false},
|
||||
{"Example", true},
|
||||
{"EXAMPLE", true},
|
||||
{"example.com/example", true},
|
||||
{";Example", false},
|
||||
{":Example", false},
|
||||
{",Example", false},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
err := ValidateLabelName(c.In)
|
||||
if err != nil && c.Valid {
|
||||
t.Fatalf("Unexpected error: %v, for '%v'", err, c.In)
|
||||
}
|
||||
if err == nil && !c.Valid {
|
||||
t.Fatalf("Expected error for invalid entry: %v", c.In)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLabelValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
In string
|
||||
Valid bool
|
||||
}{
|
||||
{"", true},
|
||||
{"*", false},
|
||||
{"example", true},
|
||||
{"example-com", true},
|
||||
{"example.com", true},
|
||||
{"-example-com", false},
|
||||
{"example-com-", false},
|
||||
{"Example", true},
|
||||
{"EXAMPLE", true},
|
||||
{"example.com/example", false},
|
||||
{";Example", false},
|
||||
{":Example", false},
|
||||
{",Example", false},
|
||||
{"{{env.EXAMPLE}}", true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
err := ValidateLabelValue(c.In)
|
||||
if err != nil && c.Valid {
|
||||
t.Fatalf("Unexpected error: %v, for '%v'", err, c.In)
|
||||
}
|
||||
if err == nil && !c.Valid {
|
||||
t.Fatalf("Expected error for invalid entry: %v", c.In)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue