appmanager: list and update app instances
diff --git a/core/appmanager/sveltekit/appmanager/src/lib/ConfigurationForm.svelte b/core/appmanager/sveltekit/appmanager/src/lib/ConfigurationForm.svelte
index 4b1844c..fe628e9 100644
--- a/core/appmanager/sveltekit/appmanager/src/lib/ConfigurationForm.svelte
+++ b/core/appmanager/sveltekit/appmanager/src/lib/ConfigurationForm.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
-import { createEventDispatcher } from 'svelte';
-
- import { derived, writable, type Writable } from 'svelte/store'
+ import { createEventDispatcher } from 'svelte';
import NetworkSelector from "./NetworkSelector.svelte";
import TextInput from "./TextInput.svelte";
+ const dispatch = createEventDispatcher();
+
export let availableNetworks = [
{
name: "Public",
@@ -17,41 +17,32 @@
},
];
export let schema = null;
+ export let value: Record<string, unknown> = {};
+ export let readonly: boolean = false;
+
+ function update(k: string, v: unknown) {
+ value[k] = v;
+ dispatch("change", value);
+ }
+
+ function updater(key: string) {
+ return (v) => update(key, v.detail);
+ }
const isNetwork = (schema): boolean => {
return "$ref" in schema &&
typeof schema["$ref"] === "string" &&
schema["$ref"] === "#/definitions/network";
};
-
- type Data = Record<string, Writable<any>>;
-
- const children = Object.fromEntries(Object.entries(schema.properties).map(([field, fieldSchema]) => {
- switch (fieldSchema.type) {
- case "object":
- return [field, writable<Data | undefined>(undefined)];
- default:
- return [field, writable<string | undefined>(field)];
- }
- }));
- const data = derived(Object.values(children), ($values) => {
- return Object.fromEntries(Object.keys(children).map((field, index) => ([
- field,
- $values[index],
- ])));
- });
-
- const dispatch = createEventDispatcher();
- $: dispatch("change", $data);
</script>
{#each Object.entries(schema.properties) as [name, schema]}
{#if schema.type === "object"}
- <svelte:self schema />
+ <svelte:self {readonly} {schema} on:change={updater(name)} />
{:else if isNetwork(schema)}
- <NetworkSelector {name} {availableNetworks} on:input={(v) => children[name].set(v.detail)} />
+ <NetworkSelector {readonly} {name} value={value[name]} {availableNetworks} on:input={updater(name)} />
{:else if schema.type === "string"}
- <TextInput {name} on:input={(v) => children[name].set(v.detail)} />
+ <TextInput {readonly} {name} value={value[name]} on:input={updater(name)} />
{/if}
{/each}
diff --git a/core/appmanager/sveltekit/appmanager/src/lib/NetworkSelector.svelte b/core/appmanager/sveltekit/appmanager/src/lib/NetworkSelector.svelte
index 57df66b..7274da3 100644
--- a/core/appmanager/sveltekit/appmanager/src/lib/NetworkSelector.svelte
+++ b/core/appmanager/sveltekit/appmanager/src/lib/NetworkSelector.svelte
@@ -6,9 +6,13 @@
export let name = "";
export let availableNetworks = [];
export let value: string | number | undefined | null = undefined;
+ export let readonly: boolean = false;
const { root } = createLabel();
- const { label, trigger, option, isSelected } = createSelect();
+ const { label, trigger, option, isSelected } = createSelect({
+ label: value,
+ disabled: readonly,
+ });
const triggerWithoutRole = derived(trigger, ($trigger) => {
const {role: _, ...rest} = $trigger;
@@ -24,8 +28,8 @@
<label use:root.action>
<span>{name}</span>
</label>
-<details role="list">
- <summary aria-haspopup="listbox" {...$triggerWithoutRole} use:trigger.action>{$label || "Select network"}</summary>
+<details role="list" {readonly}>
+ <summary {readonly} aria-haspopup="listbox" {...$triggerWithoutRole} use:trigger.action>{$label || "Select network"}</summary>
<ul role="listbox">
{#each availableNetworks as n}
<li {...$option({ value: n.name, label: n.name })} use:option.action>
diff --git a/core/appmanager/sveltekit/appmanager/src/lib/TextInput.svelte b/core/appmanager/sveltekit/appmanager/src/lib/TextInput.svelte
index b104a1c..81bcd10 100644
--- a/core/appmanager/sveltekit/appmanager/src/lib/TextInput.svelte
+++ b/core/appmanager/sveltekit/appmanager/src/lib/TextInput.svelte
@@ -5,6 +5,7 @@
export let name: string = "";
export let prefix: string = "";
export let value: string | undefined = undefined;
+ export let readonly: boolean = false;
const { root } = createLabel();
@@ -17,4 +18,4 @@
<label use:root.action for={name} class="font-medium">
<span>{name}</span>
</label>
-<input type="text" {id} {name} bind:value />
+<input type="text" {id} {name} bind:value {readonly} />
diff --git a/core/appmanager/sveltekit/appmanager/src/routes/+page.svelte b/core/appmanager/sveltekit/appmanager/src/routes/+page.svelte
index c75ac34..6314f15 100644
--- a/core/appmanager/sveltekit/appmanager/src/routes/+page.svelte
+++ b/core/appmanager/sveltekit/appmanager/src/routes/+page.svelte
@@ -2,32 +2,20 @@
import { onMount } from "svelte";
import Icon from '@iconify/svelte';
- type app = {
- name: string;
- slug: string;
- icon: string;
- shortDescription: string;
- };
-
- let apps: app[] = [];
-
- onMount(async () => {
- const resp = await fetch("/api/app-repo");
- apps = await resp.json();
- });
-
- let cur = null;
- const view = (e) => {
- if (cur === e.target) {
- cur = null;
- return;
- }
- console.log(111)
- console.log(cur?.parentElement);
- cur?.parentElement.toggleAttribute("open");
- cur = e.target;
+ type App = {
+ name: string;
+ slug: string;
+ icon: string;
+ shortDescription: string;
};
+ let apps: App[] = [];
+
+ onMount(async () => {
+ const resp = await fetch("/api/app-repo");
+ apps = await resp.json();
+ });
+
const search = (e) => {
console.log(e.target.value);
};
diff --git "a/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.svelte" "b/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.svelte"
index 946f08f..c55c339 100644
--- "a/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.svelte"
+++ "b/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.svelte"
@@ -1,13 +1,10 @@
<script lang="ts">
import { onMount } from "svelte";
- import { SubmitForm } from "@restspace/svelte-schema-form";
- import "@restspace/svelte-schema-form/css/layout.scss";
- // import "@restspace/svelte-schema-form/css/basic-skin.scss";
import Icon from '@iconify/svelte';
import toast from "svelte-french-toast";
import ConfigurationForm from "$lib/ConfigurationForm.svelte";
-import { writable } from "svelte/store";
+ import { writable } from "svelte/store";
export let data: AppData;
let config: Record<string, any> = null;
@@ -45,7 +42,6 @@
const extractDefaultValues = (schema) => {
switch (schema.type) {
- case "string": return schema.default ?? "";
case "object": {
const ret: Record<string, any> = {};
for (const [key, value] of Object.entries(schema.properties)) {
@@ -53,17 +49,12 @@
};
return ret;
}
+ default: return schema.default ?? "";
}
};
onMount(() => {
- data.config = null; // TODO(giolekva): remove
- if (data.config != null) {
- config = data.config;
- } else {
- config = extractDefaultValues(data.schema);
- console.log(config);
- }
+ config = extractDefaultValues(data.schema);
render(config);
});
@@ -79,6 +70,15 @@
<input type="submit" value="Install" />
</form>
+You have {data.instances.length} installations
+{#each data.instances as inst}
+ <details>
+ <summary>{inst.id}</summary>
+ <ConfigurationForm schema={data.schema} value={inst.config.Values} readonly={true} />
+ <a href="/instances/{inst.id}" role="button">View</a>
+ </details>
+{/each}
+
<style>
pre {
white-space: pre-wrap; /* Since CSS 2.1 */
diff --git "a/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.ts" "b/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.ts"
index 0f355f9..bb72da1 100644
--- "a/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.ts"
+++ "b/core/appmanager/sveltekit/appmanager/src/routes/app/\133slug\135/+page.ts"
@@ -5,7 +5,7 @@
icon: string;
slug: string;
schema: Record<string, unknown>;
- config: Record<string, unknown>;
+ instances: unknown[];
};
export async function load({ params, fetch }): Promise<AppData> {
@@ -17,6 +17,6 @@
icon: ret.icon,
slug: params.slug,
schema: JSON.parse(ret.schema),
- config: ret.config,
+ instances: ret.instances,
};
}
diff --git "a/core/appmanager/sveltekit/appmanager/src/routes/instance/\133slug\135/+page.svelte" "b/core/appmanager/sveltekit/appmanager/src/routes/instance/\133slug\135/+page.svelte"
new file mode 100644
index 0000000..89c180f
--- /dev/null
+++ "b/core/appmanager/sveltekit/appmanager/src/routes/instance/\133slug\135/+page.svelte"
@@ -0,0 +1,83 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import Icon from '@iconify/svelte';
+ import toast from "svelte-french-toast";
+
+ import ConfigurationForm from "$lib/ConfigurationForm.svelte";
+ import { writable } from "svelte/store";
+
+ export let data;
+ let config: Record<string, any> = null;
+ let readme: string = null;
+
+ const submit = async (config) => {
+ const resp = await fetch(`/api/instance/${data.slug}/update`, {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(config),
+ });
+ if (resp.status === 200) {
+ toast.success("Installed");
+ } else {
+ toast.error("Installation failed");
+ }
+ return false;
+ };
+
+ const render = async (config) => {
+ console.log(config);
+ const resp = await fetch(`/api/app/${data.appSlug}/render`, {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(config),
+ });
+ const app = await resp.json();
+ readme = app.readme;
+ };
+
+ const extractDefaultValues = (schema) => {
+ switch (schema.type) {
+ case "object": {
+ const ret: Record<string, any> = {};
+ for (const [key, value] of Object.entries(schema.properties)) {
+ ret[key] = extractDefaultValues(value);
+ };
+ return ret;
+ }
+ default: return schema.default ?? "";
+ }
+ };
+
+ onMount(() => {
+ config = extractDefaultValues(data.schema);
+ render(config);
+ });
+
+ const formData = writable(null);
+ $: render($formData);
+</script>
+
+<h1><Icon icon="{data.icon}" width="50" height="50" />{data.name}</h1>
+<pre>{readme}</pre>
+
+<form on:submit={() => submit($formData)}>
+ <ConfigurationForm schema={data.schema} value={data.instances[0].config.Values} on:change={(e) => formData.set(e.detail)} />
+ <input type="submit" value="Update" />
+</form>
+
+<style>
+ pre {
+ white-space: pre-wrap; /* Since CSS 2.1 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+ background-color: transparent;
+ }
+</style>
diff --git "a/core/appmanager/sveltekit/appmanager/src/routes/instance/\133slug\135/+page.ts" "b/core/appmanager/sveltekit/appmanager/src/routes/instance/\133slug\135/+page.ts"
new file mode 100644
index 0000000..f77ef68
--- /dev/null
+++ "b/core/appmanager/sveltekit/appmanager/src/routes/instance/\133slug\135/+page.ts"
@@ -0,0 +1,24 @@
+export const ssr = false;
+
+export type AppData = {
+ name: string;
+ icon: string;
+ slug: string;
+ appSlug: string;
+ schema: Record<string, unknown>;
+ instances: unknown[];
+};
+
+export async function load({ params, fetch }): Promise<AppData> {
+ const resp = await fetch(`/api/instance/${params.slug}`);
+ const ret = await resp.json();
+ console.log(ret);
+ return {
+ name: ret.name,
+ icon: ret.icon,
+ slug: params.slug,
+ appSlug: ret.instances[0].id,
+ schema: JSON.parse(ret.schema),
+ instances: ret.instances,
+ };
+}
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index fccf0b2..c0a619f 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -1,14 +1,13 @@
package installer
import (
- "fmt"
"io/ioutil"
"path/filepath"
"sigs.k8s.io/yaml"
)
-const appDirName = "apps"
+const appDir = "/apps"
const configFileName = "config.yaml"
const kustomizationFileName = "kustomization.yaml"
@@ -28,16 +27,24 @@
return m.repoIO.ReadConfig()
}
-func (m *AppManager) AppConfig(name string) (map[string]any, error) {
- configF, err := m.repoIO.Reader(fmt.Sprintf("%s/%s/%s", appDirName, name, configFileName))
+func (m *AppManager) FindAllInstances(name string) ([]AppConfig, error) {
+ return m.repoIO.FindAllInstances(appDir, name)
+}
+
+func (m *AppManager) FindInstance(name string) (AppConfig, error) {
+ return m.repoIO.FindInstance(appDir, name)
+}
+
+func (m *AppManager) AppConfig(name string) (AppConfig, error) {
+ configF, err := m.repoIO.Reader(filepath.Join(appDir, name, configFileName))
if err != nil {
- return nil, err
+ return AppConfig{}, err
}
defer configF.Close()
- var cfg map[string]any
+ var cfg AppConfig
contents, err := ioutil.ReadAll(configF)
if err != nil {
- return cfg, err
+ return AppConfig{}, err
}
err = yaml.UnmarshalStrict(contents, &cfg)
return cfg, err
@@ -77,9 +84,30 @@
"Namespace": namespaces[0],
}
}
- // TODO(giolekva): use ns suffix for app directory
return m.repoIO.InstallApp(
app,
- filepath.Join("/apps", app.Name+suffix),
+ filepath.Join(appDir, app.Name+suffix),
all)
}
+
+func (m *AppManager) Update(app App, instanceId string, config map[string]any) error {
+ // if err := m.repoIO.Fetch(); err != nil {
+ // return err
+ // }
+ globalConfig, err := m.repoIO.ReadConfig()
+ if err != nil {
+ return err
+ }
+ instanceDir := filepath.Join(appDir, instanceId)
+ instanceConfigPath := filepath.Join(instanceDir, configFileName)
+ appConfig, err := m.repoIO.ReadAppConfig(instanceConfigPath)
+ if err != nil {
+ return err
+ }
+ all := map[string]any{
+ "Global": globalConfig.Values,
+ "Values": config,
+ "Release": appConfig.Config["Release"],
+ }
+ return m.repoIO.InstallApp(app, instanceDir, all)
+}
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 8cb6cad..90f2c4b 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -101,6 +101,8 @@
e.POST("/api/app/:slug/render", s.handleAppRender)
e.POST("/api/app/:slug/install", s.handleAppInstall)
e.GET("/api/app/:slug", s.handleApp)
+ e.GET("/api/instance/:slug", s.handleInstance)
+ e.POST("/api/instance/:slug/update", s.handleAppUpdate)
webapp, err := url.Parse("http://localhost:5173")
if err != nil {
panic(err)
@@ -113,12 +115,12 @@
}
type app struct {
- Name string `json:"name"`
- Icon string `json:"icon"`
- ShortDescription string `json:"shortDescription"`
- Slug string `json:"slug"`
- Schema string `json:"schema"`
- Config any `json:"config"`
+ Name string `json:"name"`
+ Icon string `json:"icon"`
+ ShortDescription string `json:"shortDescription"`
+ Slug string `json:"slug"`
+ Schema string `json:"schema"`
+ Instances []installer.AppConfig `json:"instances,omitempty"`
}
func (s *server) handleAppRepo(c echo.Context) error {
@@ -128,8 +130,7 @@
}
resp := make([]app, len(all))
for i, a := range all {
- config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
- resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config}
+ resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil}
}
return c.JSON(http.StatusOK, resp)
}
@@ -140,8 +141,42 @@
if err != nil {
return err
}
- config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
- return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config["Values"]})
+ instances, err := s.m.FindAllInstances(slug)
+ if err != nil {
+ return err
+ }
+ return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, instances})
+}
+
+func (s *server) handleInstance(c echo.Context) error {
+ slug := c.Param("slug")
+ instance, err := s.m.FindInstance(slug)
+ if err != nil {
+ return err
+ }
+ values, ok := instance.Config["Values"].(map[string]any)
+ if !ok {
+ return fmt.Errorf("Expected map")
+ }
+ for k, v := range values {
+ if k == "Network" {
+ n, ok := v.(map[string]any)
+ if !ok {
+ return fmt.Errorf("Expected map")
+ }
+ values["Network"], ok = n["Name"]
+ if !ok {
+ return fmt.Errorf("Missing Name")
+ }
+ break
+ }
+
+ }
+ a, err := s.r.Find(instance.Id)
+ if err != nil {
+ return err
+ }
+ return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, []installer.AppConfig{instance}})
}
type file struct {
@@ -265,6 +300,41 @@
return c.String(http.StatusOK, "Installed")
}
+func (s *server) handleAppUpdate(c echo.Context) error {
+ slug := c.Param("slug")
+ appConfig, err := s.m.AppConfig(slug)
+ if err != nil {
+ return err
+ }
+ contents, err := ioutil.ReadAll(c.Request().Body)
+ if err != nil {
+ return err
+ }
+ var values map[string]any
+ if err := json.Unmarshal(contents, &values); err != nil {
+ return err
+ }
+ a, err := s.r.Find(appConfig.Id)
+ if err != nil {
+ return err
+ }
+ config, err := s.m.Config()
+ if err != nil {
+ return err
+ }
+ if network, ok := values["Network"]; ok {
+ for _, n := range createNetworks(config) {
+ if n.Name == network { // TODO(giolekva): handle not found
+ values["Network"] = n
+ }
+ }
+ }
+ if err := s.m.Update(a.App, slug, values); err != nil {
+ return err
+ }
+ return c.String(http.StatusOK, "Installed")
+}
+
func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) {
return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
URL: address,
diff --git a/core/installer/config.go b/core/installer/config.go
index 6712894..9f547ff 100644
--- a/core/installer/config.go
+++ b/core/installer/config.go
@@ -1,12 +1,5 @@
package installer
-import (
- "io"
- "io/ioutil"
-
- "sigs.k8s.io/yaml"
-)
-
type Config struct {
Values Values `json:"values"`
}
@@ -30,13 +23,3 @@
// PiholeOAuth2ClientSecret string `json:"piholeOAuth2ClientSecret,omitempty"`
// PiholeOAuth2CookieSecret string `json:"piholeOAuth2CookieSecret,omitempty"`
}
-
-func ReadConfig(r io.Reader) (Config, error) {
- var cfg Config
- contents, err := ioutil.ReadAll(r)
- if err != nil {
- return cfg, err
- }
- err = yaml.UnmarshalStrict(contents, &cfg)
- return cfg, err
-}
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index 671bfb8..b01cd82 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -5,6 +5,7 @@
"fmt"
"io"
"io/fs"
+ "io/ioutil"
"net"
"path"
"path/filepath"
@@ -21,6 +22,7 @@
type RepoIO interface {
Fetch() error
ReadConfig() (Config, error)
+ ReadAppConfig(path string) (AppConfig, error)
ReadKustomization(path string) (*Kustomization, error)
WriteKustomization(path string, kust Kustomization) error
WriteYaml(path string, data any) error
@@ -30,6 +32,8 @@
CreateDir(path string) error
RemoveDir(path string) error
InstallApp(app App, path string, values map[string]any) error
+ FindAllInstances(root string, name string) ([]AppConfig, error)
+ FindInstance(root string, name string) (AppConfig, error)
}
type repoIO struct {
@@ -62,7 +66,26 @@
return Config{}, err
}
defer configF.Close()
- return ReadConfig(configF)
+ var cfg Config
+ if err := readYaml(configF, &cfg); err != nil {
+ return Config{}, err
+ } else {
+ return cfg, nil
+ }
+}
+
+func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
+ configF, err := r.Reader(path)
+ if err != nil {
+ return AppConfig{}, err
+ }
+ defer configF.Close()
+ var cfg AppConfig
+ if err := readYaml(configF, &cfg); err != nil {
+ return AppConfig{}, err
+ } else {
+ return cfg, nil
+ }
}
func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
@@ -158,6 +181,11 @@
return err
}
+type AppConfig struct {
+ Id string `json:"id"`
+ Config map[string]any `json:"config"`
+}
+
func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any) error {
if !filepath.IsAbs(appRootDir) {
return fmt.Errorf("Expected absolute path: %s", appRootDir)
@@ -188,7 +216,11 @@
if err := r.CreateDir(appRootDir); err != nil {
return err
}
- if err := r.WriteYaml(path.Join(appRootDir, configFileName), values); err != nil {
+ cfg := AppConfig{
+ Id: app.Name,
+ Config: values,
+ }
+ if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
return err
}
}
@@ -212,6 +244,43 @@
return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
}
+func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
+ if !filepath.IsAbs(root) {
+ return nil, fmt.Errorf("Expected absolute path: %s", root)
+ }
+ kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]AppConfig, 0)
+ for _, app := range kust.Resources {
+ cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
+ if err != nil {
+ return nil, err
+ }
+ if cfg.Id == name {
+ ret = append(ret, cfg)
+ }
+ }
+ return ret, nil
+}
+
+func (r *repoIO) FindInstance(root string, name string) (AppConfig, error) {
+ if !filepath.IsAbs(root) {
+ return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
+ }
+ kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
+ if err != nil {
+ return AppConfig{}, err
+ }
+ for _, app := range kust.Resources {
+ if app == name {
+ return r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
+ }
+ }
+ return AppConfig{}, nil
+}
+
func auth(signer ssh.Signer) *gitssh.PublicKeys {
return &gitssh.PublicKeys{
Signer: signer,
@@ -224,3 +293,11 @@
},
}
}
+
+func readYaml[T any](r io.Reader, o *T) error {
+ if contents, err := ioutil.ReadAll(r); err != nil {
+ return err
+ } else {
+ return yaml.UnmarshalStrict(contents, o)
+ }
+}
diff --git a/core/installer/values-tmpl/rpuppy.jsonschema b/core/installer/values-tmpl/rpuppy.jsonschema
index 5fa2fed..e21b570 100644
--- a/core/installer/values-tmpl/rpuppy.jsonschema
+++ b/core/installer/values-tmpl/rpuppy.jsonschema
@@ -10,7 +10,7 @@
},
"type": "object",
"properties": {
- "Network": { "$ref": "#/definitions/network" },
+ "Network": { "$ref": "#/definitions/network", "default": "Public" },
"Subdomain": { "type": "string", "default": "woof" }
},
"additionalProperties": false