Skip to content

Commit

Permalink
WIP: cli: Introduce an upgrade command
Browse files Browse the repository at this point in the history
  • Loading branch information
olix0r committed Mar 26, 2019
1 parent 437bcfb commit 50e2109
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 10 deletions.
10 changes: 9 additions & 1 deletion cli/cmd/inject.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,15 @@ func (options *injectOptions) fetchConfigsOrDefault() (*config.All, error) {
}

// overrideConfigs uses command-line overrides to update the provided configs
func (options *injectOptions) overrideConfigs(configs *config.All) {
func (options *proxyConfigOptions) overrideConfigs(configs *config.All) {
if options.linkerdVersion != "" {
configs.Global.Version = options.linkerdVersion
}

if len(options.ignoreInboundPorts) > 0 {
configs.Proxy.IgnoreInboundPorts = toPorts(options.ignoreInboundPorts)
}

if len(options.ignoreInboundPorts) > 0 {
configs.Proxy.IgnoreInboundPorts = toPorts(options.ignoreInboundPorts)
}
Expand Down
23 changes: 15 additions & 8 deletions cli/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,15 @@ func newInstallOptionsWithDefaults() *installOptions {
disableExternalProfiles: false,
noInitContainer: false,
},
identityOptions: &installIdentityOptions{
trustDomain: defaultIdentityTrustDomain,
issuanceLifetime: defaultIdentityIssuanceLifetime,
clockSkewAllowance: defaultIdentityClockSkewAllowance,
},
identityOptions: newInstallIdentityOptionsWithDefaults(),
}
}

func newInstallIdentityOptionsWithDefaults() *installIdentityOptions {
return &installIdentityOptions{
trustDomain: defaultIdentityTrustDomain,
issuanceLifetime: defaultIdentityIssuanceLifetime,
clockSkewAllowance: defaultIdentityClockSkewAllowance,
}
}

Expand All @@ -186,10 +190,14 @@ func newCmdInstall() *cobra.Command {
if err != nil {
return err
}
return render(values, os.Stdout, configs)
return values.render(os.Stdout, configs)
},
}

return options.configure(cmd)
}

func (options *installOptions) configure(cmd *cobra.Command) *cobra.Command {
addProxyConfigFlags(cmd, options.proxyConfigOptions)
cmd.PersistentFlags().UintVar(
&options.controllerReplicas, "controller-replicas", options.controllerReplicas,
Expand Down Expand Up @@ -239,7 +247,6 @@ func newCmdInstall() *cobra.Command {
&options.identityOptions.issuanceLifetime, "identity-issuance-lifetime", options.identityOptions.issuanceLifetime,
"The amount of time for which the Identity issuer should certify identity",
)

return cmd
}

Expand Down Expand Up @@ -357,7 +364,7 @@ func toPromLogLevel(level string) string {
}
}

func render(values *installValues, w io.Writer, configs *pb.All) error {
func (values *installValues) render(w io.Writer, configs *pb.All) error {
// Render raw values and create chart config
rawValues, err := yaml.Marshal(values)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestRender(t *testing.T) {
controlPlaneNamespace = tc.configs.GetGlobal().GetLinkerdNamespace()

var buf bytes.Buffer
if err := render(tc.values, &buf, tc.configs); err != nil {
if err := tc.values.render(&buf, tc.configs); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
diffTestdata(t, tc.goldenFileName, buf.String())
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func init() {
RootCmd.AddCommand(newCmdTap())
RootCmd.AddCommand(newCmdTop())
RootCmd.AddCommand(newCmdUninject())
RootCmd.AddCommand(newCmdUpgrade())
RootCmd.AddCommand(newCmdVersion())
}

Expand Down
207 changes: 207 additions & 0 deletions cli/cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package cmd

import (
"errors"
"fmt"
"os"
"strings"

"github.com/golang/protobuf/jsonpb"
pb "github.com/linkerd/linkerd2/controller/gen/config"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/tls"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type (
upgradeOptions struct{ *installOptions }
)

func newUpgradeOptionsWithDefaults() *upgradeOptions {
return &upgradeOptions{newInstallOptionsWithDefaults()}
}

func newCmdUpgrade() *cobra.Command {
options := newUpgradeOptionsWithDefaults()

cmd := &cobra.Command{
Use: "upgrade [flags]",
Short: "Output Kubernetes configs to upgrade an existing Linkerd control plane",
Long: "Output Kubernetes configs to upgrade an existing Linkerd control plane.",
RunE: func(cmd *cobra.Command, args []string) error {
k, err := newKubernetes()
if err != nil {
return err
}

values, configs, err := options.buildFromCluster(k)
if err != nil {
return err
}

return values.render(os.Stdout, configs)
},
}

options.configure(cmd)
return cmd
}

func newKubernetes() (*kubernetes.Clientset, error) {
api, err := k8s.NewAPI(kubeconfigPath, kubeContext)
if err != nil {
return nil, err
}

return kubernetes.NewForConfig(api.Config)
}

// fetchInstallValuesFromCluster checks the kubernetes API to fetch an existing
// linkerd configuration.
//
// This bypasses the public API so that we can access secrets and validate permissions.
func (options *upgradeOptions) buildFromCluster(k *kubernetes.Clientset) (*installValues, *pb.All, error) {
if options.ignoreCluster {
return nil, nil, errors.New("--ignore-cluster cannot be used with upgrade")
}

controllerReplicas := uint(1)
identityReplicas := uint(1)
selector := fmt.Sprintf("%s in (controller, identity)", k8s.ControllerComponentLabel)
deploys, err := k.ExtensionsV1beta1().Deployments(controlPlaneNamespace).
List(metav1.ListOptions{LabelSelector: selector})
if err != nil {
return nil, nil, err
}
for _, deploy := range deploys.Items {
if deploy.Spec.Replicas == nil {
continue
}
r := uint(*deploy.Spec.Replicas)

switch deploy.GetLabels()[k8s.ControllerComponentLabel] {
case "controller":
controllerReplicas = r
case "identity":
identityReplicas = r
}
}

// Fetch the existing cluster configs or exit.
configs := &pb.All{Global: &pb.Global{}, Proxy: &pb.Proxy{}}
configMap, err := k.CoreV1().ConfigMaps(controlPlaneNamespace).
Get(k8s.ConfigConfigMapName, metav1.GetOptions{})
if err != nil {
return nil, nil, err
}
uj := jsonpb.Unmarshaler{}
if err := uj.Unmarshal(strings.NewReader(configMap.Data["global"]), configs.Global); err != nil {
return nil, nil, err
}
if err := uj.Unmarshal(strings.NewReader(configMap.Data["proxy"]), configs.Proxy); err != nil {
return nil, nil, err
}

// Override the configs from the command-line flags.
options.overrideConfigs(configs)
j := jsonpb.Marshaler{EmitDefaults: true}
globalJSON, err := j.MarshalToString(configs.GetGlobal())
if err != nil {
return nil, nil, err
}
proxyJSON, err := j.MarshalToString(configs.GetProxy())
if err != nil {
return nil, nil, err
}

values := &installValues{
// Container images:
ControllerImage: fmt.Sprintf("%s/controller:%s", options.dockerRegistry, configs.GetGlobal().GetVersion()),
WebImage: fmt.Sprintf("%s/web:%s", options.dockerRegistry, configs.GetGlobal().GetVersion()),
GrafanaImage: fmt.Sprintf("%s/grafana:%s", options.dockerRegistry, configs.GetGlobal().GetVersion()),
PrometheusImage: prometheusImage,
ImagePullPolicy: configs.Proxy.ProxyImage.PullPolicy,

// Kubernetes labels/annotations/resourcse:
CreatedByAnnotation: k8s.CreatedByAnnotation,
CliVersion: k8s.CreatedByAnnotationValue(),
ControllerComponentLabel: k8s.ControllerComponentLabel,
ProxyContainerName: k8s.ProxyContainerName,
ProxyInjectAnnotation: k8s.ProxyInjectAnnotation,
ProxyInjectDisabled: k8s.ProxyInjectDisabled,

// Controller configuration:
Namespace: controlPlaneNamespace,
UUID: configs.GetGlobal().GetInstallationUuid(),
ControllerLogLevel: options.controllerLogLevel,
PrometheusLogLevel: toPromLogLevel(options.controllerLogLevel),
ControllerReplicas: controllerReplicas,
ControllerUID: options.controllerUID,
EnableHA: options.highAvailability,
EnableH2Upgrade: !options.disableH2Upgrade,
NoInitContainer: configs.GetGlobal().GetCniEnabled(),
ProxyAutoInjectEnabled: configs.GetGlobal().GetAutoinjectContext() != nil,

GlobalConfig: globalJSON,
ProxyConfig: proxyJSON,
}

idctx := configs.GetGlobal().GetIdentityContext()
if idctx == nil {
// If we're upgrading from a version without identity, generate a new one.
i, err := newInstallIdentityOptionsWithDefaults().genValues()
if err != nil {
return nil, nil, err
}

values.Identity = i
return values, configs, nil
}

trustPEM := idctx.GetTrustAnchorsPem()
roots, err := tls.DecodePEMCertPool(trustPEM)
if err != nil {
return nil, nil, err
}

secret, err := k.CoreV1().Secrets(controlPlaneNamespace).
Get(k8s.IdentityIssuerSecretName, metav1.GetOptions{})
if err != nil {
return nil, nil, err
}

keyPEM := string(secret.Data["key.pem"])
key, err := tls.DecodePEMKey(keyPEM)
if err != nil {
return nil, nil, err
}

crtPEM := string(secret.Data["crt.pem"])
crt, err := tls.DecodePEMCrt(crtPEM)
if err != nil {
return nil, nil, fmt.Errorf("invalid issuer certificate: %s", err)
}

cred := tls.Cred{PrivateKey: key, Crt: *crt}
if err := cred.Verify(roots, ""); err != nil {
return nil, nil, fmt.Errorf("invalid issuer credentials: %s", err)
}

values.Identity = &installIdentityValues{
Replicas: identityReplicas,
TrustDomain: idctx.GetTrustDomain(),
TrustAnchorsPEM: trustPEM,
Issuer: &issuerValues{
ClockSkewAllowance: idctx.GetClockSkewAllowance().String(),
IssuanceLifetime: idctx.GetIssuanceLifetime().String(),
KeyPEM: keyPEM,
CrtPEM: crtPEM,
CrtExpiry: crt.Certificate.NotAfter,
CrtExpiryAnnotation: k8s.IdentityIssuerExpiryAnnotation,
},
}

return values, configs, nil
}
4 changes: 4 additions & 0 deletions pkg/k8s/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ const (
// volume mounted into each proxy to store identity credentials.
IdentityEndEntityVolumeName = "linkerd-identity-end-entity"

// IdentityIssuerSecretName is the name assigned the temporary end-entity
// volume mounted into each proxy to store identity credentials.
IdentityIssuerSecretName = "linkerd-identity-issuer"

// ProxyPortName is the name of the Linkerd Proxy's proxy port.
ProxyPortName = "linkerd-proxy"

Expand Down

0 comments on commit 50e2109

Please sign in to comment.