Skip to content

Commit

Permalink
Merge pull request #44 from stefanprodan/bundle-lint
Browse files Browse the repository at this point in the history
Implement `bundle lint` command
  • Loading branch information
stefanprodan authored Apr 2, 2023
2 parents d213d7e + 4d50e02 commit 3e8a4c4
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 24 deletions.
57 changes: 57 additions & 0 deletions api/v1alpha1/bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Copyright 2023 Stefan Prodan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

const (
// BundleAPIVersionSelector is the CUE path for the Timoni's bundle API version.
BundleAPIVersionSelector Selector = "bundle.apiVersion"

// BundleInstancesSelector is the CUE path for the Timoni's bundle instances.
BundleInstancesSelector Selector = "bundle.instances"

// BundleModuleURLSelector is the CUE path for the Timoni's bundle module url.
BundleModuleURLSelector Selector = "module.url"

// BundleModuleVersionSelector is the CUE path for the Timoni's bundle module version.
BundleModuleVersionSelector Selector = "module.version"

// BundleNamespaceSelector is the CUE path for the Timoni's bundle instance namespace.
BundleNamespaceSelector Selector = "namespace"

// BundleValuesSelector is the CUE path for the Timoni's bundle instance values.
BundleValuesSelector Selector = "values"
)

// BundleSchema defines the v1alpha1 CUE schema for Timoni's bundle API.
// TODO: switch to go:embed when this is available https://github.com/cue-lang/cue/issues/607
const BundleSchema = `
import "strings"
#Bundle: {
apiVersion: string & =~"^v1alpha1$"
instances: [string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63) & strings.MinRunes(1)]: {
module: close({
url: string & =~"^oci://.*$"
version: string & strings.MinRunes(3)
})
namespace: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63) & strings.MinRunes(1)
values: {...}
}
}
bundle: #Bundle
`
18 changes: 0 additions & 18 deletions api/v1alpha1/selectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,4 @@ const (

// ApplySelector is the CUE path for the Timoni's apply resource sets.
ApplySelector Selector = "timoni.apply"

// BundleAPIVersionSelector is the CUE path for the Timoni's bundle API version.
BundleAPIVersionSelector Selector = "bundle.apiVersion"

// BundleInstancesSelector is the CUE path for the Timoni's bundle instances.
BundleInstancesSelector Selector = "bundle.instances"

// BundleModuleURLSelector is the CUE path for the Timoni's bundle module url.
BundleModuleURLSelector Selector = "module.url"

// BundleModuleVersionSelector is the CUE path for the Timoni's bundle module version.
BundleModuleVersionSelector Selector = "module.version"

// BundleNamespaceSelector is the CUE path for the Timoni's bundle instance namespace.
BundleNamespaceSelector Selector = "namespace"

// BundleValuesSelector is the CUE path for the Timoni's bundle instance values.
BundleValuesSelector Selector = "values"
)
20 changes: 17 additions & 3 deletions cmd/timoni/bundle_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package main

import (
"context"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"fmt"
"os"
"os/exec"
Expand All @@ -28,7 +26,9 @@ import (
"strings"
"time"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"github.com/fluxcd/pkg/ssa"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -89,14 +89,24 @@ func init() {
}

func runBundleApplyCmd(cmd *cobra.Command, args []string) error {
bundleSchema, err := os.CreateTemp("", "schema.*.cue")
if err != nil {
return err
}
defer os.Remove(bundleSchema.Name())
if _, err := bundleSchema.WriteString(apiv1.BundleSchema); err != nil {
return err
}

ctx := cuecontext.New()

cfg := &load.Config{
Package: "_",
DataFiles: true,
}

ix := load.Instances(bundleApplyArgs.files, cfg)
files := append(bundleApplyArgs.files, bundleSchema.Name())
ix := load.Instances(files, cfg)
if len(ix) == 0 {
return fmt.Errorf("no bundle found")
}
Expand All @@ -111,6 +121,10 @@ func runBundleApplyCmd(cmd *cobra.Command, args []string) error {
return v.Err()
}

if err := v.Validate(cue.Concrete(true)); err != nil {
return err
}

apiVersion := v.LookupPath(cue.ParsePath(apiv1.BundleAPIVersionSelector.String()))
if apiVersion.Err() != nil {
return fmt.Errorf("lookup %s failed, error: %w", apiv1.BundleAPIVersionSelector.String(), apiVersion.Err())
Expand Down
7 changes: 4 additions & 3 deletions cmd/timoni/bundle_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os"
"path/filepath"
"sigs.k8s.io/controller-runtime/pkg/client"
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

. "github.com/onsi/gomega"
)

Expand Down
139 changes: 139 additions & 0 deletions cmd/timoni/bundle_lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
Copyright 2023 Stefan Prodan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"fmt"
"os"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"github.com/spf13/cobra"

apiv1 "github.com/stefanprodan/timoni/api/v1alpha1"
"github.com/stefanprodan/timoni/internal/flags"
)

var bundleLintCmd = &cobra.Command{
Use: "lint",
Short: "Validate bundle definitions",
Long: `The bundle lint command validates that a bundle definition conforms with Timoni's schema.'.
`,
Example: ` # Validate a bundle
timoni bundle lint -f bundle.cue
# Validate a bundle defined in multiple files
timoni bundle lint \
-f ./bundle.cue \
-f ./bundle_secrets.cue
`,
RunE: runBundleLintCmd,
}

type bundleLintFlags struct {
pkg flags.Package
files []string
}

var bundleLintArgs bundleLintFlags

func init() {
bundleLintCmd.Flags().VarP(&bundleLintArgs.pkg, bundleLintArgs.pkg.Type(), bundleLintArgs.pkg.Shorthand(), bundleLintArgs.pkg.Description())
bundleLintCmd.Flags().StringSliceVarP(&bundleLintArgs.files, "file", "f", nil,
"The local path to bundle.cue files.")
bundleCmd.AddCommand(bundleLintCmd)
}

func runBundleLintCmd(cmd *cobra.Command, args []string) error {
bundleSchema, err := os.CreateTemp("", "schema.*.cue")
if err != nil {
return err
}
defer os.Remove(bundleSchema.Name())
if _, err := bundleSchema.WriteString(apiv1.BundleSchema); err != nil {
return err
}

ctx := cuecontext.New()

cfg := &load.Config{
Package: "_",
DataFiles: true,
}

files := append(bundleLintArgs.files, bundleSchema.Name())
ix := load.Instances(files, cfg)
if len(ix) == 0 {
return fmt.Errorf("no bundle found")
}

inst := ix[0]
if inst.Err != nil {
return fmt.Errorf("bundle error: %w", inst.Err)
}

v := ctx.BuildInstance(inst)
if v.Err() != nil {
return v.Err()
}

if err := v.Validate(cue.Concrete(true)); err != nil {
return err
}

apiVersion := v.LookupPath(cue.ParsePath(apiv1.BundleAPIVersionSelector.String()))
if apiVersion.Err() != nil {
return fmt.Errorf("lookup %s failed, error: %w", apiv1.BundleAPIVersionSelector.String(), apiVersion.Err())
}

apiVer, _ := apiVersion.String()
if apiVer != apiv1.GroupVersion.Version {
return fmt.Errorf("API version %s not supported, must be %s", apiVer, apiv1.GroupVersion.Version)
}

instances := v.LookupPath(cue.ParsePath(apiv1.BundleInstancesSelector.String()))
if instances.Err() != nil {
return fmt.Errorf("lookup %s failed, error: %w", apiv1.BundleInstancesSelector.String(), instances.Err())
}

var instCount int
iter, _ := instances.Fields(cue.Concrete(true))
for iter.Next() {
name := iter.Selector().String()
expr := iter.Value()

namespace := expr.LookupPath(cue.ParsePath(apiv1.BundleNamespaceSelector.String()))
if namespace.Err() != nil {
return fmt.Errorf("lookup %s failed, error: %w", apiv1.BundleNamespaceSelector.String(), instances.Err())
}

if _, err := namespace.String(); err != nil {
return fmt.Errorf("invalid %s, error: %w", apiv1.BundleNamespaceSelector.String(), err)
}

logger.Printf("instance %s is valid", name)
instCount++
}

if instCount == 0 {
return fmt.Errorf("no instances found in bundle")
}

logger.Printf("bundle is valid")
return nil
}
Loading

0 comments on commit 3e8a4c4

Please sign in to comment.