blob: e438a2679bd9e75d4eac4a77f695be015d243b2c [file] [log] [blame]
package installer
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"net/http"
"path"
"path/filepath"
"sigs.k8s.io/yaml"
)
const appDir = "/apps"
const configFileName = "config.yaml"
const kustomizationFileName = "kustomization.yaml"
type AppManager struct {
repoIO RepoIO
nsCreator NamespaceCreator
}
func NewAppManager(repoIO RepoIO, nsCreator NamespaceCreator) (*AppManager, error) {
return &AppManager{
repoIO,
nsCreator,
}, nil
}
func (m *AppManager) Config() (Config, error) {
var cfg Config
if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
return Config{}, err
} else {
return cfg, nil
}
}
func (m *AppManager) appConfig(path string) (AppConfig, error) {
var cfg AppConfig
if err := ReadYaml(m.repoIO, path, &cfg); err != nil {
return AppConfig{}, err
} else {
return cfg, nil
}
}
func (m *AppManager) FindAllInstances(name string) ([]AppConfig, error) {
kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
if err != nil {
return nil, err
}
ret := make([]AppConfig, 0)
for _, app := range kust.Resources {
cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
if err != nil {
return nil, err
}
cfg.Id = app
if cfg.AppId == name {
ret = append(ret, cfg)
}
}
return ret, nil
}
func (m *AppManager) FindInstance(id string) (AppConfig, error) {
kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
if err != nil {
return AppConfig{}, err
}
for _, app := range kust.Resources {
if app == id {
cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
if err != nil {
return AppConfig{}, err
}
cfg.Id = id
return cfg, nil
}
}
return AppConfig{}, nil
}
func (m *AppManager) AppConfig(name string) (AppConfig, error) {
configF, err := m.repoIO.Reader(filepath.Join(appDir, name, configFileName))
if err != nil {
return AppConfig{}, err
}
defer configF.Close()
var cfg AppConfig
contents, err := ioutil.ReadAll(configF)
if err != nil {
return AppConfig{}, err
}
err = yaml.UnmarshalStrict(contents, &cfg)
return cfg, err
}
type allocatePortReq struct {
Protocol string `json:"protocol"`
SourcePort int `json:"sourcePort"`
TargetService string `json:"targetService"`
TargetPort int `json:"targetPort"`
}
func openPorts(ports []PortForward) error {
for _, p := range ports {
var buf bytes.Buffer
req := allocatePortReq{
Protocol: p.Protocol,
SourcePort: p.SourcePort,
TargetService: p.TargetService,
TargetPort: p.TargetPort,
}
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return err
}
resp, err := http.Post(p.Allocator, "application/json", &buf)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
}
}
return nil
}
func createKustomizationChain(r RepoFS, path string) error {
for p := filepath.Clean(path); p != "/"; {
parent, child := filepath.Split(p)
kustPath := filepath.Join(parent, "kustomization.yaml")
kust, err := ReadKustomization(r, kustPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
k := NewKustomization()
kust = &k
} else {
return err
}
}
kust.AddResources(child)
if err := WriteYaml(r, kustPath, kust); err != nil {
return err
}
p = filepath.Clean(parent)
}
return nil
}
func InstallApp(repo RepoIO, nsc NamespaceCreator, app App, appDir string, namespace string, initValues map[string]any, derived Derived) error {
if err := nsc.Create(namespace); err != nil {
return err
}
derived.Release = Release{
Namespace: namespace,
RepoAddr: repo.FullAddress(),
AppDir: appDir,
}
rendered, err := app.Render(derived)
if err != nil {
return err
}
if err := openPorts(rendered.Ports); err != nil {
return err
}
return repo.Atomic(func(r RepoFS) (string, error) {
if err := createKustomizationChain(r, appDir); err != nil {
return "", err
}
{
if err := r.RemoveDir(appDir); err != nil {
return "", err
}
if err := r.CreateDir(appDir); err != nil {
return "", err
}
cfg := AppConfig{
AppId: app.Name(),
Config: initValues,
Derived: derived,
}
if err := WriteYaml(r, path.Join(appDir, configFileName), cfg); err != nil {
return "", err
}
}
{
appKust := NewKustomization()
for name, contents := range rendered.Resources {
appKust.AddResources(name)
out, err := r.Writer(path.Join(appDir, name))
if err != nil {
return "", err
}
defer out.Close()
if _, err := out.Write(contents); err != nil {
return "", err
}
}
if err := WriteYaml(r, path.Join(appDir, "kustomization.yaml"), appKust); err != nil {
return "", err
}
}
return fmt.Sprintf("install: %s", app.Name()), nil
})
}
func (m *AppManager) Install(app App, appDir string, namespace string, values map[string]any) error {
appDir = filepath.Clean(appDir)
if err := m.repoIO.Pull(); err != nil {
return err
}
globalConfig, err := m.Config()
if err != nil {
return err
}
derivedValues, err := deriveValues(values, app.Schema(), CreateNetworks(globalConfig))
if err != nil {
return err
}
derived := Derived{
Global: globalConfig.Values,
Values: derivedValues,
}
return InstallApp(m.repoIO, m.nsCreator, app, appDir, namespace, values, derived)
}
func (m *AppManager) Update(app App, instanceId string, config map[string]any) error {
if err := m.repoIO.Pull(); err != nil {
return err
}
globalConfig, err := m.Config()
if err != nil {
return err
}
instanceDir := filepath.Join(appDir, instanceId)
instanceConfigPath := filepath.Join(instanceDir, configFileName)
appConfig, err := m.appConfig(instanceConfigPath)
if err != nil {
return err
}
derivedValues, err := deriveValues(config, app.Schema(), CreateNetworks(globalConfig))
if err != nil {
return err
}
derived := Derived{
Global: globalConfig.Values,
Release: appConfig.Derived.Release,
Values: derivedValues,
}
return InstallApp(m.repoIO, m.nsCreator, app, instanceDir, appConfig.Derived.Release.Namespace, config, derived)
}
func (m *AppManager) Remove(instanceId string) error {
if err := m.repoIO.Pull(); err != nil {
return err
}
return m.repoIO.Atomic(func(r RepoFS) (string, error) {
r.RemoveDir(filepath.Join(appDir, instanceId))
kustPath := filepath.Join(appDir, "kustomization.yaml")
kust, err := ReadKustomization(r, kustPath)
if err != nil {
return "", err
}
kust.RemoveResources(instanceId)
WriteYaml(r, kustPath, kust)
return fmt.Sprintf("uninstall: %s", instanceId), nil
})
}
// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
func CreateNetworks(global Config) []Network {
return []Network{
{
Name: "Public",
IngressClass: fmt.Sprintf("%s-ingress-public", global.Values.PCloudEnvName),
CertificateIssuer: fmt.Sprintf("%s-public", global.Values.Id),
Domain: global.Values.Domain,
AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public/api/allocate", global.Values.PCloudEnvName),
},
{
Name: "Private",
IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id),
Domain: global.Values.PrivateDomain,
AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private/api/allocate", global.Values.Id),
},
}
}