appmanager: refactor schema into interface, introduce cuelang
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
new file mode 100644
index 0000000..d43809c
--- /dev/null
+++ b/core/installer/app_test.go
@@ -0,0 +1,296 @@
+package installer
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/cuecontext"
+ fluxcd "github.com/fluxcd/source-controller/api/v1beta2"
+ "helm.sh/helm/v3/pkg/registry"
+ // "github.com/go-git/go-billy/v5/memfs"
+ "github.com/go-git/go-billy/v5/osfs"
+ "helm.sh/helm/v3/pkg/action"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+//go:embed values-tmpl/rpuppy.cue
+var rpuppyConfig []byte
+
+type ContainerImage struct {
+ Repository string
+ Tag string
+ PullPolicy string
+}
+
+type Chart struct {
+ Source ChartSource
+ Chart string
+}
+
+type ChartSource struct {
+ Kind string
+ Address string
+}
+
+type ApplicationConfig struct {
+ Images map[string]ContainerImage
+ Charts map[string]Chart
+}
+
+type client struct {
+ clientset dynamic.Interface
+}
+
+func (c *client) CreateHelmChart(chart fluxcd.HelmChart) error {
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(chart); err != nil {
+ return nil
+ }
+ var u unstructured.Unstructured
+ if err := json.NewDecoder(&buf).Decode(&u.Object); err != nil {
+ return err
+ }
+ _, err := c.clientset.Resource(schema.GroupVersionResource{Group: fluxcd.GroupVersion.Group, Version: fluxcd.GroupVersion.Version, Resource: "helmcharts"}).Namespace(chart.Namespace).Create(context.TODO(), &u, metav1.CreateOptions{})
+ return err
+}
+
+func NewClient(kubeconfig string) (*client, error) {
+ if kubeconfig == "" {
+ config, err := rest.InClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+ c, err := dynamic.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+ return &client{c}, nil
+
+ } else {
+ config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
+ if err != nil {
+ return nil, err
+ }
+ c, err := dynamic.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+ return &client{c}, nil
+ }
+}
+
+const networkSchema = `
+#Network: {
+ IngressClass: string
+ CertificateIssuer: string
+ Domain: string
+}
+
+value: %s
+
+valid: #Network & value
+`
+
+type StringFormatter struct {
+ s strings.Builder
+}
+
+func (f *StringFormatter) Write(b []byte) (n int, err error) {
+ return f.s.Write(b)
+}
+
+func (f *StringFormatter) Width() (wid int, ok bool) {
+ return 4, true
+}
+
+func (f *StringFormatter) Precision() (prec int, ok bool) {
+ return 4, true
+}
+
+func (f *StringFormatter) Flag(c int) bool {
+ return false
+}
+
+func IsNetwork(v cue.Value) bool {
+ if v.Value().Kind() != cue.StructKind {
+ return false
+ }
+ value := fmt.Sprintf("%#v", v)
+ s := fmt.Sprintf(networkSchema, value)
+ c := cuecontext.New()
+ u := c.CompileString(s)
+ return u.Err() == nil && u.Validate() == nil
+}
+
+func PrintSchema(v cue.Value) {
+ f, _ := v.Fields()
+ for f.Next() {
+ fmt.Printf("%s\n", f.Selector())
+ if IsNetwork(f.Value()) {
+ fmt.Println("network")
+ }
+ PrintSchema(f.Value())
+ }
+}
+
+func TestInput(t *testing.T) {
+ c := cuecontext.New()
+ cfg := c.CompileBytes(rpuppyConfig)
+ input := c.CompileString(`
+global: {
+ id: "foo"
+}
+input: {
+ network: {
+ name: "public"
+ ingressClass: "dodo-ingress-public"
+ certificateIssuer: "rpuppu-public"
+ domain: "lekva.me"
+ }
+ subdomain: "rpuppy"
+}
+`)
+ if cfg.Err() != nil {
+ panic(cfg.Err())
+ }
+ if err := cfg.Validate(); err != nil {
+ panic(err)
+ }
+ PrintSchema(cfg.Eval().LookupPath(cue.ParsePath("input")))
+ out := cfg.Unify(input)
+ if out.Err() != nil {
+ panic(out.Err())
+ }
+ if err := out.Validate(); err != nil {
+ panic(err)
+ }
+ fmt.Printf("%#v\n", out)
+ e := out.Eval()
+ if e.Err() != nil {
+ panic(out.Err())
+ }
+ if err := e.Validate(); err != nil {
+ panic(err)
+ }
+ fmt.Printf("%#v\n", e)
+ fmt.Println(e.IsConcrete())
+}
+
+func TestParseApplicationConfig(t *testing.T) {
+ return
+ var r cue.Runtime
+ i, err := r.Compile("rpuppy", rpuppyConfig)
+ if err != nil {
+ panic(err)
+ }
+ var cfg ApplicationConfig
+ if err := i.Value().Decode(&cfg); err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", cfg)
+ _, err = NewClient("/Users/lekva/dev/src/pcloud/priv/kubeconfig-hetzner")
+ if err != nil {
+ panic(err)
+ }
+
+ for name, c := range cfg.Charts {
+ chart := fluxcd.HelmChart{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: fluxcd.GroupVersion.String(),
+ Kind: "HelmChart",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: "dodo",
+ },
+ Spec: fluxcd.HelmChartSpec{
+ Chart: c.Chart,
+ SourceRef: fluxcd.LocalHelmChartSourceReference{
+ Kind: c.Source.Kind,
+ Name: c.Source.Address,
+ },
+ Interval: metav1.Duration{time.Hour},
+ },
+ }
+ fmt.Printf("%+v\n", chart)
+ // if err := client.CreateHelmChart(chart); err != nil {
+ // panic(err)
+ // }
+ }
+}
+
+type downloader struct {
+ client *http.Client
+}
+
+func NewDownloader() *downloader {
+ return &downloader{
+ client: http.DefaultClient,
+ }
+}
+
+func (d *downloader) Download(addr string, out io.Writer) error {
+ resp, err := d.client.Get(addr)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(out, resp.Body); err != nil {
+ return err
+ }
+ return nil
+}
+
+func TestDownload(t *testing.T) {
+ return
+ // fs := memfs.New()
+ fs := osfs.New("/tmp")
+ func() {
+ f, err := fs.Create("/chart")
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ d := NewDownloader()
+ if err := d.Download("http://localhost:9090/helmchart/dodo/rpuppy/rpuppy-0.0.1.tgz", f); err != nil {
+ panic(err)
+ }
+ }()
+ client, err := registry.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ if err := client.Login("https://harbor.t46.lekva.me", registry.LoginOptBasicAuth("admin", "Harbor12345")); err != nil {
+ panic(err)
+ }
+ defer client.Logout("https://harbor.t46.lekva.me")
+ push := action.NewPushWithOpts(action.WithPushConfig(&action.Configuration{
+ RegistryClient: client,
+ }))
+ fmt.Printf("%+v\n", push)
+ res, err := push.Run("/tmp/chart", "oci://harbor.t46.lekva.me/library/charts")
+ fmt.Println(res)
+ if err != nil {
+ panic(err)
+ }
+ // cfg, err := ActionConfigFactory{"/Users/lekva/dev/src/pcloud/priv/kubeconfig-hetzner"}.New("")
+ // installer := action.NewInstall(config)
+ // installer.Namespace = env.Name
+ // installer.ReleaseName = "metallb-ns"
+ // installer.Wait = true
+ // installer.WaitForJobs = true
+
+}