blob: 914307c5dd6d5b5e4c729a7eed902609b8b2e7d9 [file] [log] [blame]
package tasks
import (
"fmt"
"net"
"net/netip"
"strings"
"github.com/miekg/dns"
"github.com/giolekva/pcloud/core/installer"
)
var initGroups = []string{"admin"}
func CreateRepoClient(env Env, st *state) Task {
t := newLeafTask("Create repo client", func() error {
repo, err := st.ssClient.GetRepo("config")
if err != nil {
return err
}
r := installer.NewRepoIO(repo, st.ssClient.Signer)
appManager, err := installer.NewAppManager(r, st.nsCreator)
if err != nil {
return err
}
st.appManager = appManager
st.appsRepo = installer.NewInMemoryAppRepository(installer.CreateAllApps())
st.nsGen = installer.NewPrefixGenerator(env.Name + "-")
st.emptySuffixGen = installer.NewEmptySuffixGenerator()
return nil
})
t.beforeStart = func() {
st.infoListener("Setting up core infrastructure services.")
}
return &t
}
func SetupInfra(env Env, startIP net.IP, st *state) Task {
return newConcurrentParentTask(
"Setup core services",
true,
SetupNetwork(env, startIP, st),
SetupCertificateIssuers(env, st),
SetupAuth(env, st),
SetupGroupMemberships(env, st),
SetupHeadscale(env, startIP, st),
SetupWelcome(env, st),
SetupAppStore(env, st),
)
}
func CommitEnvironmentConfiguration(env Env, st *state) Task {
t := newLeafTask("commit config", func() error {
repo, err := st.ssClient.GetRepo("config")
if err != nil {
return err
}
r := installer.NewRepoIO(repo, st.ssClient.Signer)
{
// TODO(giolekva): private domain can be configurable as well
config := installer.Config{
Values: installer.Values{
PCloudEnvName: env.PCloudEnvName,
Id: env.Name,
ContactEmail: env.ContactEmail,
Domain: env.Domain,
PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
PublicIP: st.publicIPs[0].String(),
NamespacePrefix: fmt.Sprintf("%s-", env.Name),
},
}
if err := r.WriteYaml("config.yaml", config); err != nil {
return err
}
}
{
out, err := r.Writer("pcloud-charts.yaml")
if err != nil {
return err
}
defer out.Close()
_, err = fmt.Fprintf(out, `
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: pcloud
namespace: %s
spec:
interval: 1m0s
url: https://github.com/giolekva/pcloud
ref:
branch: ingress-port-allocator
`, env.Name)
if err != nil {
return err
}
rootKust, err := r.ReadKustomization("kustomization.yaml")
if err != nil {
return err
}
rootKust.AddResources("pcloud-charts.yaml")
if err := r.WriteKustomization("kustomization.yaml", *rootKust); err != nil {
return err
}
r.CommitAndPush("configure charts repo")
}
return nil
})
return &t
}
type firstAccount struct {
Created bool `json:"created"`
Groups []string `json:"groups"`
}
func ConfigureFirstAccount(env Env, st *state) Task {
t := newLeafTask("Configure first account settings", func() error {
repo, err := st.ssClient.GetRepo("config")
if err != nil {
return err
}
r := installer.NewRepoIO(repo, st.ssClient.Signer)
fa := firstAccount{false, initGroups}
if err := r.WriteYaml("first-account.yaml", fa); err != nil {
return err
}
return r.CommitAndPush("first account membership configuration")
})
return &t
}
func SetupNetwork(env Env, startIP net.IP, st *state) Task {
t := newLeafTask("Setup private and public networks", func() error {
startAddr, err := netip.ParseAddr(startIP.String())
if err != nil {
return err
}
if !startAddr.Is4() {
return fmt.Errorf("Expected IPv4, got %s instead", startAddr)
}
addr := startAddr.AsSlice()
if addr[3] != 0 {
return fmt.Errorf("Expected last byte to be zero, got %d instead", addr[3])
}
addr[3] = 10
fromIP, ok := netip.AddrFromSlice(addr)
if !ok {
return fmt.Errorf("Must not reach")
}
addr[3] = 254
toIP, ok := netip.AddrFromSlice(addr)
if !ok {
return fmt.Errorf("Must not reach")
}
{
ingressPrivateIP := startAddr
headscaleIP := ingressPrivateIP.Next()
app, err := st.appsRepo.Find("metallb-ipaddresspool")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
"name": fmt.Sprintf("%s-ingress-private", env.Name),
"from": ingressPrivateIP.String(),
"to": ingressPrivateIP.String(),
"autoAssign": false,
"namespace": "metallb-system",
}); err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
"name": fmt.Sprintf("%s-headscale", env.Name),
"from": headscaleIP.String(),
"to": headscaleIP.String(),
"autoAssign": false,
"namespace": "metallb-system",
}); err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"name": env.Name,
"from": fromIP.String(),
"to": toIP.String(),
"autoAssign": false,
"namespace": "metallb-system",
}); err != nil {
return err
}
}
{
keys, err := installer.NewSSHKeyPair("port-allocator")
if err != nil {
return err
}
user := fmt.Sprintf("%s-port-allocator", env.Name)
if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
return err
}
if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
return err
}
app, err := st.appsRepo.Find("private-network")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"privateNetwork": map[string]any{
"hostname": "private-network-proxy",
"username": "private-network-proxy",
"ipSubnet": fmt.Sprintf("%s/24", startIP.String()),
},
"sshPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
return err
}
}
return nil
})
return &t
}
func SetupCertificateIssuers(env Env, st *state) Task {
pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
app, err := st.appsRepo.Find("certificate-issuer-public")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{}); err != nil {
return err
}
return nil
})
priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
app, err := st.appsRepo.Find("certificate-issuer-private")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"apiConfigMap": map[string]any{
"name": "api-config", // TODO(gio): take from global pcloud config
"namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
},
}); err != nil {
return err
}
return nil
})
return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
}
func SetupAuth(env Env, st *state) Task {
t := newLeafTask("Setup", func() error {
app, err := st.appsRepo.Find("core-auth")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
}); err != nil {
return err
}
return nil
})
return newSequentialParentTask(
"Authentication services",
false,
&t,
waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
)
}
func SetupGroupMemberships(env Env, st *state) Task {
t := newLeafTask("Setup", func() error {
app, err := st.appsRepo.Find("memberships")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"authGroups": strings.Join(initGroups, ","),
}); err != nil {
return err
}
return nil
})
return newSequentialParentTask(
"Group membership",
false,
&t,
waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
)
}
func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
t := newLeafTask("Setup", func() error {
app, err := st.appsRepo.Find("headscale")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"subdomain": "headscale",
"ipSubnet": fmt.Sprintf("%s/24", startIP),
}); err != nil {
return err
}
return nil
})
return newSequentialParentTask(
"Setup mesh VPN",
false,
&t,
waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
)
}
func SetupWelcome(env Env, st *state) Task {
t := newLeafTask("Setup", func() error {
keys, err := installer.NewSSHKeyPair("welcome")
if err != nil {
return err
}
user := fmt.Sprintf("%s-welcome", env.Name)
if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
return err
}
if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
return err
}
app, err := st.appsRepo.Find("welcome")
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
return err
}
return nil
})
return newSequentialParentTask(
"Welcome service",
false,
&t,
waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
)
}
func SetupAppStore(env Env, st *state) Task {
t := newLeafTask("Application marketplace", func() error {
user := fmt.Sprintf("%s-appmanager", env.Name)
keys, err := installer.NewSSHKeyPair(user)
if err != nil {
return err
}
if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
return err
}
if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
return err
}
app, err := st.appsRepo.Find("app-manager") // TODO(giolekva): configure
if err != nil {
return err
}
if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
"authGroups": strings.Join(initGroups, ","),
}); err != nil {
return err
}
return nil
})
return &t
}
type DNSSecKey struct {
Basename string `json:"basename,omitempty"`
Key []byte `json:"key,omitempty"`
Private []byte `json:"private,omitempty"`
DS []byte `json:"ds,omitempty"`
}
func newDNSSecKey(zone string) (DNSSecKey, error) {
key := &dns.DNSKEY{
Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
}
priv, err := key.Generate(256)
if err != nil {
return DNSSecKey{}, err
}
return DNSSecKey{
Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
Key: []byte(key.String()),
Private: []byte(key.PrivateKeyString(priv)),
DS: []byte(key.ToDS(dns.SHA256).String()),
}, nil
}