Skip to content

Commit

Permalink
Merge pull request #64 from Peefy/feat-external-depends
Browse files Browse the repository at this point in the history
feat: support for external dependencies for the KRM KCL spec
  • Loading branch information
Peefy authored May 16, 2024
2 parents ec1dabc + 906ddd8 commit c317d20
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 96 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ spec:
# omit other fields
```

## Dependencies

```yaml
apiVersion: krm.kcl.dev/v1alpha1
kind: KCLRun
spec:
# Set the dependencies are the external dependencies for the KCL code.
# The format of the `dependencies` field is same as the [dependencies]` in the `kcl.mod` file
dependencies:
helloworld = 0.1.0
source: |
import helloworld
```
## Guides for Developing KCL
Here's what you can do in the KCL script:
Expand Down
13 changes: 12 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type KCLRun struct {
Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
// MatchConstraints defines the resource matching rules.
MatchConstraints api.MatchConstraintsSpec `json:"matchConstraints,omitempty" yaml:"matchConstraints,omitempty"`
// Dependencies are the external dependencies for the KCL code.
// The format of the `dependencies` field is same as the `[dependencies]` in the `kcl.mod` file
Dependencies string `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
} `json:"spec" yaml:"spec"`
}

Expand Down Expand Up @@ -170,19 +173,27 @@ func (c *KCLRun) Transform(in []*yaml.RNode, fnCfg *yaml.RNode) ([]*yaml.RNode,
if os.Getenv(SrcUrlPasswordEnvVar) != "" {
c.Spec.Credentials.Password = os.Getenv(SrcUrlPasswordEnvVar)
}
cli, err := client.NewKpmClient()
if src.IsOCI(c.Spec.Source) && c.Spec.Credentials.Url != "" {
cli, err := client.NewKpmClient()
if err != nil {
return nil, err
}
if err := cli.LoginOci(c.Spec.Credentials.Url, c.Spec.Credentials.Username, c.Spec.Credentials.Password); err != nil {
return nil, err
}
}
var dependencies []string
if c.Spec.Dependencies != "" {
dependencies, err = edit.LoadDepListFromConfig(cli, c.Spec.Dependencies)
if err != nil {
return nil, err
}
}

st := &edit.SimpleTransformer{
Name: DefaultProgramName,
Source: c.Spec.Source,
Dependencies: dependencies,
FunctionConfig: fnCfg,
Config: &c.Spec.Config,
}
Expand Down
27 changes: 23 additions & 4 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestKCLRun(t *testing.T) {
expectErrMsg string
}{
{
name: "KCLRun0",
name: "KCLRunInlineSource",
config: `apiVersion: krm.kcl.dev/v1alpha1
kind: KCLRun
metadata:
Expand All @@ -91,7 +91,7 @@ spec:
expectResult: `apiVersion: v1`,
},
{
name: "KCLRun1",
name: "KCLRunWithParams",
config: `apiVersion: krm.kcl.dev/v1alpha1
kind: KCLRun
metadata:
Expand All @@ -108,7 +108,7 @@ spec:
expectResult: `apiVersion: v1`,
},
{
name: "KCLRun2",
name: "KCLRunWithArgumentsConfig",
config: `apiVersion: krm.kcl.dev/v1alpha1
kind: KCLRun
metadata:
Expand All @@ -126,7 +126,7 @@ spec:
expectResult: `apiVersion: v1`,
},
{
name: "KCLRun3",
name: "KCLRunWithDisableNoneConfig",
config: `apiVersion: krm.kcl.dev/v1alpha1
kind: KCLRun
metadata:
Expand All @@ -143,6 +143,25 @@ spec:
`,
expectResult: `b: 1`,
},
{
name: "KCLRunWithDependencies",
config: `apiVersion: krm.kcl.dev/v1alpha1
kind: KCLRun
metadata:
name: my-kcl-fn
namespace: foo
spec:
dependencies:
helloworld = "0.1.0"
source: |
import helloworld
{
a = helloworld.The_first_kcl_program
}
`,
expectResult: `a: Hello World!`,
},
}
for _, tc := range testcases {
tc := tc
Expand Down
95 changes: 5 additions & 90 deletions pkg/edit/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"kcl-lang.io/cli/pkg/options"
"kcl-lang.io/krm-kcl/pkg/api"
"kcl-lang.io/krm-kcl/pkg/source"

Expand Down Expand Up @@ -36,7 +34,7 @@ const (
// Return:
// 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) {
return RunKCLWithConfig(name, source, resourceList, nil)
return RunKCLWithConfig(name, source, []string{}, resourceList, nil)
}

// RunKCLWithConfig runs a KCL program specified by the given source code or url,
Expand All @@ -50,7 +48,7 @@ func RunKCL(name, source string, resourceList *yaml.RNode) ([]*yaml.RNode, error
//
// Return:
// A pointer to []*yaml.RNode objects that represent the output YAML objects of the KCL program.
func RunKCLWithConfig(name, source string, resourceList *yaml.RNode, config *api.ConfigSpec) ([]*yaml.RNode, error) {
func RunKCLWithConfig(name, source string, dependencies []string, resourceList *yaml.RNode, config *api.ConfigSpec) ([]*yaml.RNode, error) {
// 1. Construct KCL code from source.
entry, err := SourceToTempEntry(source)
if err != nil {
Expand All @@ -65,6 +63,9 @@ func RunKCLWithConfig(name, source string, resourceList *yaml.RNode, config *api
result := bytes.NewBuffer([]byte{})
opts.Entries = []string{entry}
opts.Writer = result
if len(dependencies) > 0 {
opts.ExternalPackages = dependencies
}
err = opts.Run()
if err != nil {
return nil, errors.Wrap(err)
Expand Down Expand Up @@ -118,89 +119,3 @@ func SourceToTempEntry(src string) (string, error) {
return file, nil
}
}

func constructOptions(resourceList *yaml.RNode, config *api.ConfigSpec) (*options.RunOptions, 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)
}
paramsOptionKCLValue, err := ToKCLValueString(v, emptyConfig)
if err != nil {
return nil, errors.Wrap(err)
}
// 4. Read environment variables.
pathOptionKCLValue := os.Getenv("PATH")

// 5. Read Env map
envMapOptionKCLValue, err := getEnvMapOptionKCLValue()
if err != nil {
return nil, errors.Wrap(err)
}
opts := options.NewRunOptions()
opts.NoStyle = true
if config != nil {
opts.Debug = config.Debug
opts.DisableNone = config.DisableNone
opts.Overrides = config.Overrides
opts.PathSelectors = config.PathSelectors
opts.Settings = config.Settings
opts.ShowHidden = config.ShowHidden
opts.SortKeys = config.SortKeys
opts.StrictRangeCheck = config.StrictRangeCheck
opts.Vendor = config.Vendor
opts.Arguments = config.Arguments
}
opts.Arguments = append(opts.Arguments,
// resource_list
fmt.Sprintf("%s=%s", resourceListOptionName, resourceListOptionKCLValue),
// resource.items
fmt.Sprintf("%s=%s", itemsOptionName, itemsOptionKCLValue),
// resource.functionConfig.spec.params
fmt.Sprintf("%s=%s", paramsOptionName, paramsOptionKCLValue),
// environment variable example (PATH)
fmt.Sprintf("PATH=%s", pathOptionKCLValue),
// environment map example (option("env"))
fmt.Sprintf("env=%s", envMapOptionKCLValue),
)
return opts, nil
}

// getEnvMapOptionKCLValue retrieves the environment map from the KCL 'option("env")' function.
func getEnvMapOptionKCLValue() (string, error) {
envMap := make(map[string]string)
env := os.Environ()
for _, e := range env {
pair := strings.SplitN(e, "=", 2)
envMap[pair[0]] = pair[1]
}

envMapInterface := make(map[string]interface{})
for k, v := range envMap {
envMapInterface[k] = v
}

v, err := yaml.FromMap(envMapInterface)
if err != nil {
return "", errors.Wrap(err)
}

// 4. Convert the YAML RNode to its KCL value string representation.
envMapOptionKCLValue, err := ToKCLValueString(v, emptyConfig)
if err != nil {
return "", errors.Wrap(err)
}

return envMapOptionKCLValue, nil
}
137 changes: 137 additions & 0 deletions pkg/edit/opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package edit

import (
"fmt"
"os"
"path/filepath"
"strings"

"kcl-lang.io/cli/pkg/options"
"kcl-lang.io/kpm/pkg/client"
pkg "kcl-lang.io/kpm/pkg/package"
"kcl-lang.io/krm-kcl/pkg/api"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

// LoadDepsFrom parses the kcl external package option from a path.
// It will find `kcl.mod` recursively from the path, resolve deps
// in the `kcl.mod` and return the option. If not found, return the
// empty option.
func LoadDepListFromConfig(cli *client.KpmClient, dependencies string) ([]string, error) {
if cli == nil {
return nil, nil
}
cli.SetLogWriter(nil)
modData := fmt.Sprintf("[package]\n\n[dependencies]\n%s", dependencies)
// May be a inline code source.
tmpDir, err := os.MkdirTemp("", "sandbox")
if err != nil {
return nil, fmt.Errorf("error creating temp directory: %v", err)
}
// Write kcl code in the temp file.
tempFile := filepath.Join(tmpDir, "kcl.mod")
err = os.WriteFile(tempFile, []byte(modData), 0666)
if err != nil {
return nil, errors.Wrap(err)
}
pkg, err := pkg.LoadKclPkg(tmpDir)
if err != nil {
return nil, err
}
depsMap, err := cli.ResolveDepsIntoMap(pkg)
if err != nil {
return nil, err
}
deps := []string{}
for depName, depPath := range depsMap {
deps = append(deps, fmt.Sprintf("%s=%s", depName, depPath))
}
return deps, nil
}

func constructOptions(resourceList *yaml.RNode, config *api.ConfigSpec) (*options.RunOptions, 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)
}
paramsOptionKCLValue, err := ToKCLValueString(v, emptyConfig)
if err != nil {
return nil, errors.Wrap(err)
}
// 4. Read environment variables.
pathOptionKCLValue := os.Getenv("PATH")

// 5. Read Env map
envMapOptionKCLValue, err := getEnvMapOptionKCLValue()
if err != nil {
return nil, errors.Wrap(err)
}
opts := options.NewRunOptions()
opts.NoStyle = true
if config != nil {
opts.Debug = config.Debug
opts.DisableNone = config.DisableNone
opts.Overrides = config.Overrides
opts.PathSelectors = config.PathSelectors
opts.Settings = config.Settings
opts.ShowHidden = config.ShowHidden
opts.SortKeys = config.SortKeys
opts.StrictRangeCheck = config.StrictRangeCheck
opts.Vendor = config.Vendor
opts.Arguments = config.Arguments
}
opts.Arguments = append(opts.Arguments,
// resource_list
fmt.Sprintf("%s=%s", resourceListOptionName, resourceListOptionKCLValue),
// resource.items
fmt.Sprintf("%s=%s", itemsOptionName, itemsOptionKCLValue),
// resource.functionConfig.spec.params
fmt.Sprintf("%s=%s", paramsOptionName, paramsOptionKCLValue),
// environment variable example (PATH)
fmt.Sprintf("PATH=%s", pathOptionKCLValue),
// environment map example (option("env"))
fmt.Sprintf("env=%s", envMapOptionKCLValue),
)
return opts, nil
}

// getEnvMapOptionKCLValue retrieves the environment map from the KCL 'option("env")' function.
func getEnvMapOptionKCLValue() (string, error) {
envMap := make(map[string]string)
env := os.Environ()
for _, e := range env {
pair := strings.SplitN(e, "=", 2)
envMap[pair[0]] = pair[1]
}

envMapInterface := make(map[string]interface{})
for k, v := range envMap {
envMapInterface[k] = v
}

v, err := yaml.FromMap(envMapInterface)
if err != nil {
return "", errors.Wrap(err)
}

// 4. Convert the YAML RNode to its KCL value string representation.
envMapOptionKCLValue, err := ToKCLValueString(v, emptyConfig)
if err != nil {
return "", errors.Wrap(err)
}

return envMapOptionKCLValue, nil
}
Loading

0 comments on commit c317d20

Please sign in to comment.