Skip to content

Commit

Permalink
feat: simply krm-kcl usage and interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
Peefy committed Aug 10, 2023
1 parent 529cf61 commit 112d92f
Show file tree
Hide file tree
Showing 20 changed files with 122 additions and 115 deletions.
41 changes: 5 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,7 @@ diff \

## FunctionConfig

There are 2 kinds of `functionConfig` supported by this function:

+ ConfigMap
+ A custom resource of kind `KCLRun`

To use a ConfigMap as the functionConfig, the KCL script source must be specified in the data.source field. Additional parameters can be specified in the data field.

Here's an example:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: set-replicas
data:
replicas: "5"
source: |
resources = option("resource_list")
setReplicas = lambda items, replicas {
[item | {
if item.kind == "Deployment":
spec.replicas = int(replicas)
} for item in items]
}
setReplicas(resources.items or [], resources.functionConfig.data.replicas)
```
In the example above, the script accesses the replicas parameters using `option("resource_list").functionConfig.data.replicas`.

To use a KCLRun as the functionConfig, the KCL source must be specified in the source field. Additional parameters can be specified in the params field. The params field supports any complex data structure as long as it can be represented in YAML.
To use a `KCLRun` as the functionConfig, the KCL source must be specified in the source field. Additional parameters can be specified in the params field. The params field supports any complex data structure as long as it can be represented in YAML.

```yaml
apiVersion: krm.kcl.dev/v1alpha1
Expand All @@ -83,21 +54,19 @@ spec:
toAdd:
configmanagement.gke.io/managed: disabled
source: |
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
params = option("params")
toMatch = params.toMatch
toAdd = params.toAdd
[item | {
items = [item | {
# If all annotations are matched, patch more annotations
if all key, value in toMatch {
item.metadata.annotations[key] == value
}:
metadata.annotations: toAdd
} for item in items]
} for item in option("items")]
```
In the example above, the script accesses the `toMatch` parameters using `option("resource_list").functionConfig.spec.params.toMatch`.
In the example above, the script accesses the `toMatch` parameters using `option("params").toMatch`.

Besides, the `source` ield supports different KCL sources, which can come from a local file, VCS such as github, OCI registry, http, etc. You can see the specific usage [here](./pkg/options/testdata/). Take an OCI source as the example.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.1
0.4.0
2 changes: 1 addition & 1 deletion examples/abstraction/web-service/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ spec:
[deployment, if a.service: service]
}
kubernetesRender(params)
items = kubernetesRender(params)
5 changes: 2 additions & 3 deletions examples/mutation/conditionally-add-annotations/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ spec:
configmanagement.gke.io/managed: disabled
source: |
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
toMatch = params.toMatch
toAdd = params.toAdd
[item | {
items = [item | {
# If all annotations are matched, patch more annotations
if all key, value in toMatch {
item.metadata.annotations[key] == value
}:
metadata.annotations: toAdd
} for item in items]
} for item in resource.items]
5 changes: 2 additions & 3 deletions examples/mutation/conditionally-add-labels/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ spec:
configmanagement.gke.io/managed: disabled
source: |
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
toMatch = params.toMatch
toAdd = params.toAdd
[item | {
items = [item | {
# If all labels are matched, patch more labels
if all key, value in toMatch {
item.metadata.labels[key] == value
}:
metadata.labels: toAdd
} for item in items]
} for item in resource.items]
5 changes: 2 additions & 3 deletions examples/mutation/set-annotations/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ spec:
config.kubernetes.io/local-config: "true"
source: |
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
# Use `k = v` to override existing annotations
annotations = {k = v for k, v in params.annotations}
[item | {
items = [item | {
metadata.annotations: annotations
} for item in items]
} for item in resource.items]
5 changes: 2 additions & 3 deletions examples/mutation/set-labels/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ spec:
config.kubernetes.io/local-config: "true"
source: |
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
# Use `k = v` to override existing labels
labels = {k = v for k, v in params.labels}
[item | {
items = [item | {
metadata.labels: labels
} for item in items]
} for item in resource.items]
5 changes: 2 additions & 3 deletions examples/mutation/set-replicas/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ spec:
replicas: 5
source: |
resource = option("resource_list")
items = resource.items
replicas = resource.functionConfig.spec.params.replicas
setReplicas = lambda items, replicas: int {
setReplicas = lambda items: [], replicas: int {
[item | {
if item.kind == "Deployment":
spec.replicas = replicas
} for item in items]
}
setReplicas(resources.items or [], replicas)
items = setReplicas(resources.items or [], replicas)
3 changes: 1 addition & 2 deletions examples/validation/external-ips/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ spec:
source: |
# Construct resource and params
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
# Define the validation function
validate_external_ips = lambda item, allowedIps: [str] {
Expand All @@ -26,4 +25,4 @@ spec:
item
}
# Validate All resource
[validate_external_ips(i, params.allowedIps or []) for i in items]
items = [validate_external_ips(i, params.allowedIps or []) for i in resource.items]
3 changes: 1 addition & 2 deletions examples/validation/https-only/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ spec:
source: |
# Construct resource and params
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
# Define the validation function
validate_https_only = lambda item {
Expand All @@ -25,4 +24,4 @@ spec:
item
}
# Validate All resource
[validate_https_only(i) for i in items]
items = [validate_https_only(i) for i in resource.items]
3 changes: 1 addition & 2 deletions examples/validation/replica-limits/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ spec:
source: |
# Construct resource and params
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
min_replicas: int = params.min_replicas or 0
max_replicas: int = params.max_replicas or 99999
Expand All @@ -27,4 +26,4 @@ spec:
item
}
# Validate All resource
[validate_replica_limit(i, min_replicas, max_replicas) for i in items]
items = [validate_replica_limit(i, min_replicas, max_replicas) for i in resource.items]
3 changes: 1 addition & 2 deletions examples/validation/required-annotations/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ spec:
source: |
# Construct resource and params
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
requires = params.requires or []
# Define the validation function
Expand All @@ -33,4 +32,4 @@ spec:
item
}
# Validate All resource
[validate_required_annotations(i, requires) for i in items]
items = [validate_required_annotations(i, requires) for i in resource.items]
3 changes: 1 addition & 2 deletions examples/validation/required-labels/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ spec:
source: |
# Construct resource and params
resource = option("resource_list")
items = resource.items
params = resource.functionConfig.spec.params
requires = params.requires or []
# Define the validation function
Expand All @@ -33,4 +32,4 @@ spec:
item
}
# Validate All resource
[validate_required_labels(i, requires) for i in items]
items = [validate_required_labels(i, requires) for i in resource.items]
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ go 1.19

require (
github.com/GoogleContainerTools/kpt-functions-sdk/go/fn v0.0.0-20230427202446-3255accc518d
github.com/Masterminds/sprig/v3 v3.2.3
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/hashicorp/go-getter v1.7.1
github.com/stretchr/testify v1.8.4
Expand Down
10 changes: 0 additions & 10 deletions pkg/edit/_code.tmpl

This file was deleted.

71 changes: 53 additions & 18 deletions pkg/edit/bootstrap.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
package edit

import (
"bytes"
_ "embed"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"

kcl "kcl-lang.io/kcl-go"
src "kcl-lang.io/krm-kcl/pkg/source"

"github.com/Masterminds/sprig/v3"
"github.com/acarl005/stripansi"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

//go:embed _code.tmpl
var codeTemplateString string

var codeTemplate = template.Must(template.New("code.tmpl").Funcs(sprig.TxtFuncMap()).Parse(codeTemplateString))

const resourceListOptionName = "resource_list"
const (
resourceListOptionName = "resource_list"
itemsOptionName = "items"
paramsOptionName = "params"
emptyConfig = "{}"
emptyList = "[]"
)

// RunKCL runs a KCL program specified by the given source code or url,
// with the given resource list as input, and returns the resulting KRM resource list.
Expand All @@ -34,8 +33,8 @@ const resourceListOptionName = "resource_list"
// - resourceList: a pointer to a yaml.RNode object that represents the input KRM resource list.
//
// Return:
// A pointer to a yaml.RNode object that represents the output YAML objects of the KCL program, and an error if any.
func RunKCL(name, source string, resourceList *yaml.RNode) (*yaml.RNode, error) {
// A pointer to []*yaml.RNode objects that represent the output YAML objects of the KCL program.
func RunKCL(name, source string, resourceList *yaml.RNode) ([]*yaml.RNode, error) {
// 1. Construct KCL code from source.
file, err := SourceToTempFile(source)
if err != nil {
Expand All @@ -44,27 +43,34 @@ func RunKCL(name, source string, resourceList *yaml.RNode) (*yaml.RNode, error)
defer os.RemoveAll(file)

// 2. Construct option list.
resourceListOptionKCLValue, err := ToKCLValueString(resourceList)
opts, err := constructOptions(resourceList)
if err != nil {
return nil, errors.Wrap(err)
}

// 3. Run the KCL code.
r, err := kcl.Run(file, kcl.WithOptions(fmt.Sprintf("%s=%s", resourceListOptionName, resourceListOptionKCLValue)))
r, err := kcl.Run(file, opts...)
if err != nil {
return nil, errors.Wrap(stripansi.Strip(err.Error()))
}

// 4. Parse YAML objects.
rn, err := yaml.Parse(r.GetRawYamlResult())
reader := kio.ByteReader{
Reader: strings.NewReader(r.GetRawYamlResult()),
OmitReaderAnnotations: true,
}
rn, err := reader.Read()
if err != nil {
return nil, errors.Wrap(err)
}
return rn, nil
}

// ToKCLValueString converts YAML value to KCL top level argument json value.
func ToKCLValueString(value *yaml.RNode) (string, error) {
func ToKCLValueString(value *yaml.RNode, defaultValue string) (string, error) {
if value.IsNil() {
return defaultValue, nil
}
jsonString, err := value.MarshalJSON()
if err != nil {
return "", errors.Wrap(err)
Expand All @@ -83,18 +89,47 @@ func SourceToTempFile(source string) (string, error) {
if err != nil {
return "", errors.Wrap(err)
}
buffer := new(bytes.Buffer)
codeTemplate.Execute(buffer, &struct{ Source string }{localeSource})
// Create temp files.
tmpDir, err := os.MkdirTemp("", "sandbox")
if err != nil {
return "", fmt.Errorf("error creating temp directory: %v", err)
}
// Write kcl code in the temp file.
file := filepath.Join(tmpDir, "prog.k")
err = os.WriteFile(file, buffer.Bytes(), 0666)
err = os.WriteFile(file, []byte(localeSource), 0666)
if err != nil {
return "", errors.Wrap(err)
}
return file, nil
}

func constructOptions(resourceList *yaml.RNode) ([]kcl.Option, error) {
resourceListOptionKCLValue, err := ToKCLValueString(resourceList, emptyConfig)
if err != nil {
return nil, errors.Wrap(err)
}
v, err := resourceList.Pipe(yaml.Lookup("items"))
if err != nil {
return nil, errors.Wrap(err)
}
itemsOptionKCLValue, err := ToKCLValueString(v, emptyList)
if err != nil {
return nil, errors.Wrap(err)
}
v, err = resourceList.Pipe(yaml.Lookup("functionConfig", "spec", "params"))
if err != nil {
return nil, errors.Wrap(err)
}
paramsptionKCLValue, err := ToKCLValueString(v, emptyConfig)
if err != nil {
return nil, errors.Wrap(err)
}
opts := []kcl.Option{
kcl.WithOptions(fmt.Sprintf("%s=%s", resourceListOptionName, resourceListOptionKCLValue)),
// resource.items
kcl.WithOptions(fmt.Sprintf("%s=%s", itemsOptionName, itemsOptionKCLValue)),
// resource.functionConfig.spec.params
kcl.WithOptions(fmt.Sprintf("%s=%s", paramsOptionName, paramsptionKCLValue)),
}
return opts, nil
}
Loading

0 comments on commit 112d92f

Please sign in to comment.