feat: function name matches KService name (#317)

* feat: function name matches KService name

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>

* fix typo

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
This commit is contained in:
Zbynek Roubalik 2021-04-26 10:13:32 +02:00 committed by GitHub
parent 36926acfb7
commit 541e8586f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 117 additions and 208 deletions

View File

@ -79,7 +79,6 @@ type ListItem struct {
Namespace string `json:"namespace" yaml:"namespace"`
Runtime string `json:"runtime" yaml:"runtime"`
URL string `json:"url" yaml:"url"`
KService string `json:"kservice" yaml:"kservice"`
Ready string `json:"ready" yaml:"ready"`
}
@ -108,7 +107,6 @@ type Describer interface {
type Description struct {
Name string `json:"name" yaml:"name"`
Image string `json:"image" yaml:"image"`
KService string `json:"kservice" yaml:"kservice"`
Namespace string `json:"namespace" yaml:"namespace"`
Routes []string `json:"routes" yaml:"routes"`
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`

View File

@ -52,7 +52,13 @@ kn func create --trigger events myfunc
}
func runCreate(cmd *cobra.Command, args []string) error {
config := newCreateConfig(args).Prompt()
config := newCreateConfig(args)
if err := utils.ValidateFunctionName(config.Name); err != nil {
return err
}
config = config.Prompt()
function := bosonFunc.Function{
Name: config.Name,
@ -131,12 +137,21 @@ func (c createConfig) Prompt() createConfig {
return c
}
derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.Path, prompt.WithRequired(true)))
var derivedName, derivedPath string
for {
derivedName, derivedPath = deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.Path, prompt.WithRequired(true)))
err := utils.ValidateFunctionName(derivedName)
if err == nil {
break
}
fmt.Println("Error:", err)
}
return createConfig{
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Trigger: prompt.ForString("Trigger", c.Trigger),
// Templates intentiopnally omitted from prompt for being an edge case.
// Templates intentionally omitted from prompt for being an edge case.
}
}

View File

@ -116,8 +116,6 @@ func (d description) Human(w io.Writer) error {
fmt.Fprintf(w, " %v\n", d.Name)
fmt.Fprintln(w, "Function is built in image:")
fmt.Fprintf(w, " %v\n", d.Image)
fmt.Fprintln(w, "Function is deployed as Knative Service:")
fmt.Fprintf(w, " %v\n", d.KService)
fmt.Fprintln(w, "Function is deployed in namespace:")
fmt.Fprintf(w, " %v\n", d.Namespace)
fmt.Fprintln(w, "Routes:")
@ -138,7 +136,6 @@ func (d description) Human(w io.Writer) error {
func (d description) Plain(w io.Writer) error {
fmt.Fprintf(w, "Name %v\n", d.Name)
fmt.Fprintf(w, "Image %v\n", d.Image)
fmt.Fprintf(w, "Knative Service %v\n", d.KService)
fmt.Fprintf(w, "Namespace %v\n", d.Namespace)
for _, route := range d.Routes {

View File

@ -117,9 +117,9 @@ func (items listItems) Plain(w io.Writer) error {
tabWriter := tabwriter.NewWriter(w, 0, 8, 2, ' ', 0)
defer tabWriter.Flush()
fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\t%s\n", "NAME", "NAMESPACE", "RUNTIME", "URL", "KSERVICE", "READY")
fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\n", "NAME", "NAMESPACE", "RUNTIME", "URL", "READY")
for _, item := range items {
fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\t%s\n", item.Name, item.Namespace, item.Runtime, item.URL, item.KService, item.Ready)
fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\n", item.Name, item.Namespace, item.Runtime, item.URL, item.Ready)
}
return nil
}

View File

@ -4,6 +4,8 @@
Creates a new Function project at _`path`_. If _`path`_ is unspecified, assumes the current directory. If _`path`_ does not exist, it will be created. The function name is the name of the leaf directory at path. The user can specify the runtime and trigger with flags.
Function name must consist of lower case alphanumeric characters or '-', and must 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])?').
Similar `kn` command: none.
```console

View File

@ -45,14 +45,14 @@ The unit of deployment in Boson Functions is an [OCI](https://opencontainers.org
container image, typically referred to as a Docker container image.
In order for the `func` CLI to manage these containers, you'll need to be
logged in to a container registry. For example, `docker.io/lanceball`
logged in to a container registry. For example, `docker.io/developer`
```bash
# Typically, this will log you in to docker hub if you
# omit <registry.url>. If you are using a registry
# other than Docker hub, provide that for <registry.url>
docker login -u lanceball -p [redacted] <registry.url>
docker login -u developer -p [redacted] <registry.url>
```
> Note: many of the `func` CLI commands take a `--registry` argument.
@ -62,20 +62,21 @@ docker login -u lanceball -p [redacted] <registry.url>
```bash
# This should be set to a registry that you have write permission
# on and you have logged into in the previous step.
export FUNC_REGISTRY=docker.io/lanceball
export FUNC_REGISTRY=docker.io/developer
```
## Creating a Project
With your Knative enabled cluster up and running, you can now create a new
Function Project. Let's start by creating a project directory. Function names
in `func` correspond to URLs at the moment, and there are some finicky cases
at the moment. To ensure that everything works as it should, create a project
directory consisting of three URL parts. Here is a good one.
Function Project. Let's start by creating a project directory. Function name
must consist of lower case alphanumeric characters or '-',
and must 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])?`).
```bash
mkdir fn.example.io
cd fn.example.io
mkdir fn-example-io
cd fn-example-io
```
Now, we will create the project files, build a container, and
@ -84,7 +85,6 @@ deploy the function as a Knative service.
```bash
func create -l node
func build
func deploy
```
@ -93,9 +93,15 @@ all of the defaults inferred from your environment, for example`$FUNC_REGISTRY`.
When the command has completed, you can see the deployed function.
```bash
kn service list
NAME URL LATEST AGE CONDITIONS READY REASON
fn-example-io http://fn-example-io.func.127.0.0.1.nip.io fn-example-io-ngswh-1 24s 3 OK / 3 True
func describe
Function name:
fn-example-io
Function is built in image:
docker.io/developer/fn-example-io:latest
Function is deployed in namespace:
default
Routes:
http://fn-example-io-default.apps.functions.my-cluster.com
```
Clicking on the URL will take you to the running function in your cluster. You
@ -108,7 +114,7 @@ should see a simple response.
You can add query parameters to the request to see those echoed in return.
```console
curl "http://fn-example-io.func.127.0.0.1.nip.io?name=tiger"
curl "http://fn-example-io-default.apps.functions.my-cluster.com?name=tiger"
{"query":{"name":"tiger"},"name":"tiger"}
```
@ -150,7 +156,7 @@ You might see a message such as this.
Error: remover failed to delete the service: timeout: service 'fn-example-io' not ready after 30 seconds.
```
If you do, just run `kn service list` to see if the function is still deployed.
If you do, just run `func list` to see if the function is still deployed.
It might just take a little time for it to be removed.
Now, let's clean up the current directory.
@ -168,9 +174,9 @@ cluster, use the `create` command.
func create -l node -t http
```
You can also create a Quarkus or a Golang project by providing `quarkus` or `go`
respectively to the `-l` flag. To create a project with a template for
CloudEvents, provide `events` to the `-t` flag.
You can also create a Quarkus, SpringBoot, Python or a Golang project by providing
`quarkus`, `springboot`, `python` or `go` respectively to the `-l` flag.
To create a project with a template for CloudEvents, provide `events` to the `-t` flag.
### `func build`

View File

@ -1,77 +0,0 @@
package k8s
import (
"errors"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
)
// ToK8sAllowedName converts a name to a name that's allowed for k8s service.
// k8s does not support service names with dots. So encode it such that
// www.my-domain,com -> www-my--domain-com
// Input errors if not a 1035 label.
// "a DNS-1035 label must consist of lower case alphanumeric characters or '-',
// start with an alphabetic character, and end with an alphanumeric character"
func ToK8sAllowedName(in string) (string, error) {
out := []rune{}
for _, c := range in {
// convert dots to hyphens
if c == '.' {
out = append(out, '-')
} else if c == '-' {
out = append(out, '-')
out = append(out, '-')
} else {
out = append(out, c)
}
}
result := string(out)
if errs := validation.IsDNS1035Label(result); len(errs) > 0 {
return "", errors.New(strings.Join(errs, ","))
}
return result, nil
}
// FromK8sAllowedName converts a name which has been encoded as
// an allowed k8s name using the algorithm of ToK8sAllowedName back to the original.
// www-my--domain-com -> www.my-domain.com
// Input errors if not a 1035 label.
func FromK8sAllowedName(in string) (string, error) {
if errs := validation.IsDNS1035Label(in); len(errs) > 0 {
return "", errors.New(strings.Join(errs, ","))
}
rr := []rune(in)
out := []rune{}
for i := 0; i < len(rr); i++ {
c := rr[i]
if c == '-' {
// If the next rune is either nonexistent
// or not also a dash, this is an encoded dot.
if i+1 == len(rr) || rr[i+1] != '-' {
out = append(out, '.')
continue
}
// If the next rune is also a dash, this is
// an escaping dash, so append a slash, and
// increment the pointer such that the next
// loop begins with the next potential tuple.
if rr[i+1] == '-' {
out = append(out, '-')
i++
continue
}
}
out = append(out, c)
}
return string(out), nil
}

View File

@ -1,58 +0,0 @@
// +build !integration
package k8s
import "testing"
// TestToK8sAllowedName ensures that a valid name is
// encoded into k8s allowed name.
func TestToK8sAllowedName(t *testing.T) {
cases := []struct {
In string
Out string
Err bool
}{
{"", "", true}, // invalid name
{"*", "", true}, // invalid name
{"example", "example", true},
{"example.com", "example-com", false},
{"my-domain.com", "my--domain-com", false},
}
for _, c := range cases {
out, err := ToK8sAllowedName(c.In)
if err != nil && !c.Err {
t.Fatalf("Unexpected error: %v, for '%v', want '%v', but got '%v'", err, c.In, c.Out, out)
}
if out != c.Out {
t.Fatalf("expected '%v' to yield '%v', got '%v'", c.In, c.Out, out)
}
}
}
// TestFromK8sAllowedName ensures that an allowed k8s name is correctly
// decoded back into the original name.
func TestFromK8sAllowedName(t *testing.T) {
cases := []struct {
In string
Out string
Err bool
}{
{"", "", true}, // invalid subdomain
{"*", "", true}, // invalid subdomain
{"example-com", "example.com", false},
{"my--domain-com", "my-domain.com", false},
{"cdn----1-my--domain-com", "cdn--1.my-domain.com", false},
}
for _, c := range cases {
out, err := FromK8sAllowedName(c.In)
if err != nil && !c.Err {
t.Fatalf("Unexpected error: %v", err)
}
if out != c.Out {
t.Fatalf("expected '%v' to yield '%v', got '%v'", c.In, c.Out, out)
}
}
}

View File

@ -17,7 +17,6 @@ import (
v1 "knative.dev/serving/pkg/apis/serving/v1"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/k8s"
)
type Deployer struct {
@ -40,27 +39,20 @@ func NewDeployer(namespaceOverride string) (deployer *Deployer, err error) {
func (d *Deployer) Deploy(ctx context.Context, f bosonFunc.Function) (err error) {
// k8s does not support service names with dots. so encode it such that
// www.my-domain,com -> www-my--domain-com
serviceName, err := k8s.ToK8sAllowedName(f.Name)
if err != nil {
return
}
client, err := NewServingClient(d.Namespace)
if err != nil {
return
}
_, err = client.GetService(serviceName)
_, err = client.GetService(f.Name)
if err != nil {
if errors.IsNotFound(err) {
// Let's create a new Service
if d.Verbose {
fmt.Printf("Creating Knative Service: %v\n", serviceName)
fmt.Printf("Creating Knative Service: %v\n", f.Name)
}
service, err := generateNewService(serviceName, f.ImageWithDigest(), f.Runtime, f.EnvVars)
service, err := generateNewService(f.Name, f.ImageWithDigest(), f.Runtime, f.EnvVars)
if err != nil {
err = fmt.Errorf("knative deployer failed to generate the service: %v", err)
return err
@ -74,13 +66,13 @@ func (d *Deployer) Deploy(ctx context.Context, f bosonFunc.Function) (err error)
if d.Verbose {
fmt.Println("Waiting for Knative Service to become ready")
}
err, _ = client.WaitForService(serviceName, DefaultWaitingTimeout, wait.NoopMessageCallback())
err, _ = client.WaitForService(f.Name, DefaultWaitingTimeout, wait.NoopMessageCallback())
if err != nil {
err = fmt.Errorf("knative deployer failed to wait for the service to become ready: %v", err)
return err
}
route, err := client.GetRoute(serviceName)
route, err := client.GetRoute(f.Name)
if err != nil {
err = fmt.Errorf("knative deployer failed to get the route: %v", err)
return err
@ -94,13 +86,13 @@ func (d *Deployer) Deploy(ctx context.Context, f bosonFunc.Function) (err error)
}
} else {
// Update the existing Service
err = client.UpdateServiceWithRetry(serviceName, updateService(f.ImageWithDigest(), f.EnvVars), 3)
err = client.UpdateServiceWithRetry(f.Name, updateService(f.ImageWithDigest(), f.EnvVars), 3)
if err != nil {
err = fmt.Errorf("knative deployer failed to update the service: %v", err)
return err
}
route, err := client.GetRoute(serviceName)
route, err := client.GetRoute(f.Name)
if err != nil {
err = fmt.Errorf("knative deployer failed to get the route: %v", err)
return err

View File

@ -6,7 +6,6 @@ import (
"knative.dev/eventing/pkg/apis/eventing/v1beta1"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/k8s"
)
type Describer struct {
@ -31,11 +30,6 @@ func NewDescriber(namespaceOverride string) (describer *Describer, err error) {
// www.example-site.com -> www-example--site-com
func (d *Describer) Describe(name string) (description bosonFunc.Description, err error) {
serviceName, err := k8s.ToK8sAllowedName(name)
if err != nil {
return
}
servingClient, err := NewServingClient(d.namespace)
if err != nil {
return
@ -46,12 +40,12 @@ func (d *Describer) Describe(name string) (description bosonFunc.Description, er
return
}
service, err := servingClient.GetService(serviceName)
service, err := servingClient.GetService(name)
if err != nil {
return
}
routes, err := servingClient.ListRoutes(v1.WithService(serviceName))
routes, err := servingClient.ListRoutes(v1.WithService(name))
if err != nil {
return
}
@ -87,11 +81,10 @@ func (d *Describer) Describe(name string) (description bosonFunc.Description, er
}
}
description.KService = serviceName
description.Name = name
description.Namespace = d.namespace
description.Routes = routeURLs
description.Subscriptions = subscriptions
description.Name, err = k8s.FromK8sAllowedName(service.Name)
return
}

View File

@ -7,7 +7,6 @@ import (
"knative.dev/pkg/apis"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/k8s"
)
const (
@ -46,13 +45,6 @@ func (l *Lister) List(context.Context) (items []bosonFunc.ListItem, err error) {
for _, service := range lst.Items {
// Convert the "subdomain-encoded" (i.e. kube-service-friendly) name
// back out to a fully qualified service name.
name, err := k8s.FromK8sAllowedName(service.Name)
if err != nil {
return items, err
}
// get status
ready := corev1.ConditionUnknown
for _, con := range service.Status.Conditions {
@ -63,10 +55,9 @@ func (l *Lister) List(context.Context) (items []bosonFunc.ListItem, err error) {
}
listItem := bosonFunc.ListItem{
Name: name,
Name: service.Name,
Namespace: service.Namespace,
Runtime: service.Labels["boson.dev/runtime"],
KService: service.Name,
URL: service.Status.URL.String(),
Ready: string(ready),
}

View File

@ -4,8 +4,6 @@ import (
"context"
"fmt"
"time"
"github.com/boson-project/func/k8s"
)
const RemoveTimeout = 120 * time.Second
@ -28,19 +26,14 @@ type Remover struct {
func (remover *Remover) Remove(ctx context.Context, name string) (err error) {
serviceName, err := k8s.ToK8sAllowedName(name)
if err != nil {
return
}
client, err := NewServingClient(remover.Namespace)
if err != nil {
return
}
fmt.Printf("Removing Knative Service: %v\n", serviceName)
fmt.Printf("Removing Knative Service: %v\n", name)
err = client.DeleteService(serviceName, RemoveTimeout)
err = client.DeleteService(name, RemoveTimeout)
if err != nil {
err = fmt.Errorf("knative remover failed to delete the service: %v", err)
}

File diff suppressed because one or more lines are too long

25
utils/names.go Normal file
View File

@ -0,0 +1,25 @@
package utils
import (
"errors"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
)
// 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])?')
func ValidateFunctionName(name string) error {
if errs := validation.IsDNS1123Label(name); len(errs) > 0 {
// In case of invalid name the error is this:
// "a DNS-1123 label must consist of lower case alphanumeric characters or '-',
// and must 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])?')"
// Let's reuse it for our purposes, ie. replace "DNS-1123 label" substring with "function name"
return errors.New(strings.Replace(strings.Join(errs, ""), "a DNS-1123 label", "Function name", 1))
}
return nil
}

32
utils/names_test.go Normal file
View File

@ -0,0 +1,32 @@
// +build !integration
package utils
import "testing"
// TestValidateFunctionName tests that only correct function names are accepted
func TestValidateFunctionName(t *testing.T) {
cases := []struct {
In string
Valid bool
}{
{"", false},
{"*", false},
{"-", false},
{"example", true},
{"example-com", true},
{"example.com", false},
{"-example-com", false},
{"example-com-", false},
{"Example", false},
{"EXAMPLE", false},
}
for _, c := range cases {
err := ValidateFunctionName(c.In)
if err != nil && c.Valid {
t.Fatalf("Unexpected error: %v, for '%v'", err, c.In)
}
}
}