1 package cmd
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path"
11 "time"
12
13 "github.com/linkerd/linkerd2/multicluster/static"
14 multicluster "github.com/linkerd/linkerd2/multicluster/values"
15 "github.com/linkerd/linkerd2/pkg/charts"
16 partials "github.com/linkerd/linkerd2/pkg/charts/static"
17 "github.com/linkerd/linkerd2/pkg/flags"
18 "github.com/linkerd/linkerd2/pkg/healthcheck"
19 "github.com/linkerd/linkerd2/pkg/version"
20 log "github.com/sirupsen/logrus"
21 "github.com/spf13/cobra"
22 "helm.sh/helm/v3/pkg/chart/loader"
23 "helm.sh/helm/v3/pkg/chartutil"
24 valuespkg "helm.sh/helm/v3/pkg/cli/values"
25 "helm.sh/helm/v3/pkg/engine"
26 kerrors "k8s.io/apimachinery/pkg/api/errors"
27 "sigs.k8s.io/yaml"
28 )
29
30 type (
31 multiclusterInstallOptions struct {
32 gateway multicluster.Gateway
33 remoteMirrorCredentials bool
34 }
35 )
36
37 var TemplatesMulticluster = []string{
38 chartutil.ChartfileName,
39 chartutil.ValuesfileName,
40 "templates/namespace.yaml",
41 "templates/gateway.yaml",
42 "templates/gateway-policy.yaml",
43 "templates/psp.yaml",
44 "templates/remote-access-service-mirror-rbac.yaml",
45 "templates/link-crd.yaml",
46 "templates/service-mirror-policy.yaml",
47 }
48
49 func newMulticlusterInstallCommand() *cobra.Command {
50 options, err := newMulticlusterInstallOptionsWithDefault()
51 var ha bool
52 var wait time.Duration
53 var valuesOptions valuespkg.Options
54 var ignoreCluster bool
55 var cniEnabled bool
56
57 if err != nil {
58 fmt.Fprintln(os.Stderr, err)
59 os.Exit(1)
60 }
61
62 cmd := &cobra.Command{
63 Use: "install",
64 Short: "Output Kubernetes configs to install the Linkerd multicluster add-on",
65 Args: cobra.NoArgs,
66 Example: ` # Default install.
67 linkerd multicluster install | kubectl apply -f -
68
69 The installation can be configured by using the --set, --values, --set-string and --set-file flags.
70 A full list of configurable values can be found at https://github.com/linkerd/linkerd2/blob/main/multicluster/charts/linkerd-multicluster/README.md
71 `,
72 RunE: func(cmd *cobra.Command, _ []string) error {
73 if !ignoreCluster {
74
75 hc := healthcheck.NewWithCoreChecks(&healthcheck.Options{
76 ControlPlaneNamespace: controlPlaneNamespace,
77 KubeConfig: kubeconfigPath,
78 KubeContext: kubeContext,
79 Impersonate: impersonate,
80 ImpersonateGroup: impersonateGroup,
81 APIAddr: apiAddr,
82 RetryDeadline: time.Now().Add(wait),
83 })
84 hc.RunWithExitOnError()
85 cniEnabled = hc.CNIEnabled
86 }
87 return install(cmd.Context(), stdout, options, valuesOptions, ha, ignoreCluster, cniEnabled)
88 },
89 }
90
91 flags.AddValueOptionsFlags(cmd.Flags(), &valuesOptions)
92 cmd.Flags().BoolVar(&options.gateway.Enabled, "gateway", options.gateway.Enabled, "If the gateway component should be installed")
93 cmd.Flags().Uint32Var(&options.gateway.Port, "gateway-port", options.gateway.Port, "The port on the gateway used for all incoming traffic")
94 cmd.Flags().Uint32Var(&options.gateway.Probe.Seconds, "gateway-probe-seconds", options.gateway.Probe.Seconds, "The interval at which the gateway will be checked for being alive in seconds")
95 cmd.Flags().Uint32Var(&options.gateway.Probe.Port, "gateway-probe-port", options.gateway.Probe.Port, "The liveness check port of the gateway")
96 cmd.Flags().BoolVar(&options.remoteMirrorCredentials, "service-mirror-credentials", options.remoteMirrorCredentials, "Whether to install the service account which can be used by service mirror components in source clusters to discover exported services")
97 cmd.Flags().StringVar(&options.gateway.ServiceType, "gateway-service-type", options.gateway.ServiceType, "Overwrite Service type for gateway service")
98 cmd.Flags().BoolVar(&ha, "ha", false, `Install multicluster extension in High Availability mode.`)
99 cmd.Flags().DurationVar(&wait, "wait", 300*time.Second, "Wait for core control-plane components to be available")
100 cmd.Flags().BoolVar(&ignoreCluster, "ignore-cluster", false,
101 "Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)")
102
103
104 release, err := version.IsReleaseChannel(version.Version)
105 if err != nil {
106 log.Errorf("Unable to parse version: %s", version.Version)
107 }
108 if release {
109 cmd.Flags().MarkHidden("control-plane-version")
110 cmd.Flags().MarkHidden("gateway-nginx-image")
111 cmd.Flags().MarkHidden("gateway-nginx-image-version")
112 }
113
114 return cmd
115 }
116
117 func install(ctx context.Context, w io.Writer, options *multiclusterInstallOptions, valuesOptions valuespkg.Options, ha, ignoreCluster, cniEnabled bool) error {
118 values, err := buildMulticlusterInstallValues(ctx, options, ignoreCluster)
119 if err != nil {
120 return err
121 }
122
123
124 valuesOverrides, err := valuesOptions.MergeValues(nil)
125 if err != nil {
126 return err
127 }
128
129 if ha {
130 valuesOverrides, err = charts.OverrideFromFile(valuesOverrides, static.Templates, helmMulticlusterDefaultChartName, "values-ha.yaml")
131 if err != nil {
132 return err
133 }
134 }
135
136 if cniEnabled {
137 valuesOverrides["cniEnabled"] = true
138 }
139
140 return render(w, values, valuesOverrides)
141 }
142
143 func render(w io.Writer, values *multicluster.Values, valuesOverrides map[string]interface{}) error {
144 var files []*loader.BufferedFile
145 for _, template := range TemplatesMulticluster {
146 files = append(files, &loader.BufferedFile{Name: template})
147 }
148
149 var partialFiles []*loader.BufferedFile
150 for _, template := range charts.L5dPartials {
151 partialFiles = append(partialFiles,
152 &loader.BufferedFile{Name: template},
153 )
154 }
155
156
157 if err := charts.FilesReader(static.Templates, helmMulticlusterDefaultChartName+"/", files); err != nil {
158 return err
159 }
160
161
162 if err := charts.FilesReader(partials.Templates, "", partialFiles); err != nil {
163 return err
164 }
165
166
167 chart, err := loader.LoadFiles(append(files, partialFiles...))
168 if err != nil {
169 return err
170 }
171
172
173 rawValues, err := yaml.Marshal(values)
174 if err != nil {
175 return err
176 }
177
178 err = yaml.Unmarshal(rawValues, &chart.Values)
179 if err != nil {
180 return err
181 }
182
183 vals, err := chartutil.CoalesceValues(chart, valuesOverrides)
184 if err != nil {
185 return err
186 }
187
188 fullValues := map[string]interface{}{
189 "Values": vals,
190 "Release": map[string]interface{}{
191 "Namespace": defaultMulticlusterNamespace,
192 "Service": "CLI",
193 },
194 }
195
196
197 renderedTemplates, err := engine.Render(chart, fullValues)
198 if err != nil {
199 return err
200 }
201
202
203 var buf bytes.Buffer
204 for _, tmpl := range chart.Templates {
205 t := path.Join(chart.Metadata.Name, tmpl.Name)
206 if _, err := buf.WriteString(renderedTemplates[t]); err != nil {
207 return err
208 }
209 }
210 w.Write(buf.Bytes())
211 w.Write([]byte("---\n"))
212
213 return nil
214 }
215
216 func newMulticlusterInstallOptionsWithDefault() (*multiclusterInstallOptions, error) {
217 defaults, err := multicluster.NewInstallValues()
218 if err != nil {
219 return nil, err
220 }
221
222 return &multiclusterInstallOptions{
223 gateway: *defaults.Gateway,
224 remoteMirrorCredentials: true,
225 }, nil
226 }
227
228 func buildMulticlusterInstallValues(ctx context.Context, opts *multiclusterInstallOptions, ignoreCluster bool) (*multicluster.Values, error) {
229 defaults, err := multicluster.NewInstallValues()
230 if err != nil {
231 return nil, err
232 }
233
234 defaults.Gateway.Enabled = opts.gateway.Enabled
235 defaults.Gateway.Port = opts.gateway.Port
236 defaults.Gateway.Probe.Seconds = opts.gateway.Probe.Seconds
237 defaults.Gateway.Probe.Port = opts.gateway.Probe.Port
238 defaults.LinkerdNamespace = controlPlaneNamespace
239 defaults.LinkerdVersion = version.Version
240 defaults.RemoteMirrorServiceAccount = opts.remoteMirrorCredentials
241 defaults.Gateway.ServiceType = opts.gateway.ServiceType
242
243 if ignoreCluster {
244 return defaults, nil
245 }
246
247 values, err := getLinkerdConfigMap(ctx)
248 if err != nil {
249 if kerrors.IsNotFound(err) {
250 return nil, errors.New("you need Linkerd to be installed in order to install multicluster addons")
251 }
252 return nil, err
253 }
254 defaults.ProxyOutboundPort = uint32(values.Proxy.Ports.Outbound)
255 defaults.IdentityTrustDomain = values.IdentityTrustDomain
256
257 return defaults, nil
258 }
259
View as plain text