talos/internal/app/bootkube/assets.go
Andrey Smirnov a2efa44663 chore: enable gci linter
Fixes were applied automatically.

Import ordering might be questionable, but it's strict:

* stdlib
* other packages
* same package imports

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
2020-11-09 08:09:48 -08:00

277 lines
7.6 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-multierror"
"github.com/kubernetes-sigs/bootkube/pkg/tlsutil"
"github.com/talos-systems/bootkube-plugin/pkg/asset"
tnet "github.com/talos-systems/net"
"github.com/talos-systems/talos/internal/app/bootkube/images"
"github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/constants"
)
// nolint: gocyclo
func generateAssets(config config.Provider) (err error) {
// Ensure assets directory does not exist / is left over from a failed install
if err = os.RemoveAll(constants.AssetsDirectory); err != nil {
// Ignore if the directory does not exist
if !errors.Is(err, os.ErrNotExist) {
return err
}
}
peerCrt, err := ioutil.ReadFile(constants.KubernetesEtcdPeerCert)
if err != nil {
return err
}
block, _ := pem.Decode(peerCrt)
if block == nil {
return errors.New("failed to decode peer certificate")
}
peer, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("failed to parse client certificate: %w", err)
}
caCrt, err := ioutil.ReadFile(constants.KubernetesEtcdCACert)
if err != nil {
return err
}
block, _ = pem.Decode(caCrt)
if block == nil {
return errors.New("failed to decode CA certificate")
}
ca, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("failed to parse etcd CA certificate: %w", err)
}
peerKey, err := ioutil.ReadFile(constants.KubernetesEtcdPeerKey)
if err != nil {
return err
}
block, _ = pem.Decode(peerKey)
if block == nil {
return errors.New("failed to peer key")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return fmt.Errorf("failed to parse client key: %w", err)
}
etcdServer, err := url.Parse("https://127.0.0.1:2379")
if err != nil {
return err
}
podCIDRs, err := splitCIDRs(config.Cluster().Network().PodCIDR())
if err != nil {
return fmt.Errorf("failed to process Pod CIDRs: %w", err)
}
serviceCIDRs, err := splitCIDRs(config.Cluster().Network().ServiceCIDR())
if err != nil {
return fmt.Errorf("failed to process Service CIDRs: %w", err)
}
urls := []string{config.Cluster().Endpoint().Hostname()}
urls = append(urls, config.Cluster().CertSANs()...)
altNames := altNamesFromURLs(urls)
k8sCA, err := config.Cluster().CA().GetCert()
if err != nil {
return fmt.Errorf("failed to get Kubernetes CA certificate: %w", err)
}
k8sKey, err := config.Cluster().CA().GetRSAKey()
if err != nil {
return fmt.Errorf("failed to parse Kubernetes key: %w", err)
}
apiServiceIPs, err := nthIPInCIDRSet(serviceCIDRs, 1)
if err != nil {
return fmt.Errorf("failed to calculate API service IP: %w", err)
}
dnsServiceIPs, err := nthIPInCIDRSet(serviceCIDRs, 10)
if err != nil {
return fmt.Errorf("failed to calculate DNS service IP: %w", err)
}
images := images.List(config)
conf := asset.Config{
ClusterName: config.Cluster().Name(),
APIServerExtraArgs: config.Cluster().APIServer().ExtraArgs(),
ControllerManagerExtraArgs: config.Cluster().ControllerManager().ExtraArgs(),
ProxyMode: config.Cluster().Proxy().Mode(),
ProxyExtraArgs: config.Cluster().Proxy().ExtraArgs(),
SchedulerExtraArgs: config.Cluster().Scheduler().ExtraArgs(),
CACert: k8sCA,
CAPrivKey: k8sKey,
EtcdCACert: ca,
EtcdClientCert: peer,
EtcdClientKey: key,
EtcdServers: []*url.URL{etcdServer},
EtcdUseTLS: true,
ControlPlaneEndpoint: config.Cluster().Endpoint(),
LocalAPIServerPort: config.Cluster().LocalAPIServerPort(),
APIServiceIPs: apiServiceIPs,
DNSServiceIPs: dnsServiceIPs,
PodCIDRs: podCIDRs,
ServiceCIDRs: serviceCIDRs,
NetworkProvider: config.Cluster().Network().CNI().Name(),
AltNames: altNames,
Images: images,
BootstrapSecretsSubdir: "/assets/tls",
BootstrapTokenID: config.Cluster().Token().ID(),
BootstrapTokenSecret: config.Cluster().Token().Secret(),
AESCBCEncryptionSecret: config.Cluster().AESCBCEncryptionSecret(),
ClusterDomain: config.Cluster().Network().DNSDomain(),
}
if err = asset.Render(constants.AssetsDirectory, conf); err != nil {
return err
}
// If "custom" is the CNI, we expect the user to supply one or more urls that point to CNI yamls
if config.Cluster().Network().CNI().Name() == constants.CustomCNI {
if err = fetchManifests(config.Cluster().Network().CNI().URLs(), map[string]string{}); err != nil {
return err
}
}
if len(config.Cluster().ExtraManifestURLs()) > 0 {
if err = fetchManifests(config.Cluster().ExtraManifestURLs(), config.Cluster().ExtraManifestHeaderMap()); err != nil {
return err
}
}
return nil
}
func altNamesFromURLs(urls []string) *tlsutil.AltNames {
var an tlsutil.AltNames
for _, u := range urls {
ip := net.ParseIP(u)
if ip != nil {
an.IPs = append(an.IPs, ip)
continue
}
an.DNSNames = append(an.DNSNames, u)
}
return &an
}
// fetchManifests will lay down manifests in the provided urls to the bootkube assets directory.
func fetchManifests(urls []string, headers map[string]string) error {
ctx := context.Background()
var result *multierror.Error
for _, url := range urls {
fileName := path.Base(url)
pwd, err := os.Getwd()
if err != nil {
result = multierror.Append(result, err)
continue
}
// Disable netrc since we don't have getent installed, and most likely
// never will.
httpGetter := &getter.HttpGetter{
Netrc: false,
Client: http.DefaultClient,
}
httpGetter.Header = make(http.Header)
for k, v := range headers {
httpGetter.Header.Add(k, v)
}
getter.Getters["http"] = httpGetter
getter.Getters["https"] = httpGetter
// We will squirrel all user-supplied manifests into a `zzz-talos` directory.
// Bootkube applies manifests alphabetically, so pushing these into a subdir with this name
// allows us to ensure they're the last things that get applied and things like PSPs and whatnot are present
client := &getter.Client{
Ctx: ctx,
Src: url,
Dst: filepath.Join(constants.AssetsDirectory, "manifests", "zzz-talos", fileName),
Pwd: pwd,
Mode: getter.ClientModeFile,
Options: []getter.ClientOption{},
}
fmt.Printf("Downloading manifest: %q -> %q", client.Src, client.Dst)
if err = client.Get(); err != nil {
result = multierror.Append(result, fmt.Errorf("error fetching manifest by URL %q: %w", url, err))
continue
}
}
return result.ErrorOrNil()
}
func splitCIDRs(cidrList string) (out []*net.IPNet, err error) {
for _, podCIDR := range strings.Split(cidrList, ",") {
_, cidr, err := net.ParseCIDR(podCIDR)
if err != nil {
return nil, fmt.Errorf("failed to parse %q as a CIDR: %w", podCIDR, err)
}
out = append(out, cidr)
}
return out, nil
}
func nthIPInCIDRSet(cidrList []*net.IPNet, offset int) (out []net.IP, err error) {
for _, cidr := range cidrList {
ip, err := tnet.NthIPInNetwork(cidr, offset)
if err != nil {
return nil, fmt.Errorf("failed to calculate offset %d from CIDR %q: %w", offset, cidr, err)
}
out = append(out, ip)
}
return out, nil
}