1// Copyright 2019 Istio Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package multicluster
16
17import (
18	"fmt"
19	"io/ioutil"
20	"sync"
21	"time"
22
23	"github.com/gogo/protobuf/types"
24	"github.com/golang/sync/errgroup"
25	"github.com/spf13/cobra"
26	"github.com/spf13/pflag"
27	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28
29	"istio.io/api/mesh/v1alpha1"
30	iop "istio.io/api/operator/v1alpha1"
31	operatorV1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
32	"istio.io/istio/operator/pkg/util"
33	"istio.io/istio/operator/pkg/validate"
34
35	"istio.io/istio/pilot/pkg/serviceregistry"
36	"istio.io/istio/pkg/util/protomarshal"
37)
38
39// defaults the user can override
40func defaultControlPlane() *operatorV1alpha1.IstioOperator {
41	return &operatorV1alpha1.IstioOperator{
42		Kind:       "IstioOperator",
43		ApiVersion: "install.istio.io/v1alpha1",
44		ObjectMeta: metav1.ObjectMeta{
45			Name: "default",
46		},
47		Spec: &iop.IstioOperatorSpec{
48			Profile: "default",
49		},
50	}
51}
52
53// overlay configuration which will override user config.
54func overlayIstioControlPlane(mesh *Mesh, current *Cluster, meshNetworks *v1alpha1.MeshNetworks) ( // nolint: interfacer
55	*operatorV1alpha1.IstioOperator, error) {
56	meshNetworksJSON, err := protomarshal.ToJSONMap(meshNetworks)
57	if err != nil {
58		return nil, err
59	}
60	untypedValues := map[string]interface{}{
61		"global": map[string]interface{}{
62			"meshID": mesh.meshID,
63		},
64	}
65	typedValues := &operatorV1alpha1.Values{
66		Gateways: &operatorV1alpha1.GatewaysConfig{
67			IstioIngressgateway: &operatorV1alpha1.IngressGatewayConfig{
68				Env: map[string]interface{}{
69					"ISTIO_MESH_NETWORK": current.Network,
70				},
71			},
72		},
73		Global: &operatorV1alpha1.GlobalConfig{
74			ControlPlaneSecurityEnabled: &types.BoolValue{Value: true},
75			MeshNetworks:                meshNetworksJSON,
76			MultiCluster: &operatorV1alpha1.MultiClusterConfig{
77				ClusterName: current.clusterName,
78			},
79			Network: current.Network,
80		},
81	}
82
83	typedValuesJSON, err := protomarshal.ToJSONMap(typedValues)
84	if err != nil {
85		return nil, err
86	}
87
88	return &operatorV1alpha1.IstioOperator{
89		Kind:       "IstioOperator",
90		ApiVersion: "install.istio.io/v1alpha1",
91		ObjectMeta: metav1.ObjectMeta{
92			Name: "default",
93		},
94		Spec: &iop.IstioOperatorSpec{
95			Values:            typedValuesJSON,
96			UnvalidatedValues: untypedValues,
97		},
98	}, nil
99}
100
101func generateIstioControlPlane(mesh *Mesh, current *Cluster, meshNetworks *v1alpha1.MeshNetworks, from string) (string, error) { // nolint:interfacer
102	var base *operatorV1alpha1.IstioOperator
103	if from != "" {
104		b, err := ioutil.ReadFile(from)
105		if err != nil {
106			return "", err
107		}
108		var user operatorV1alpha1.IstioOperator
109		if err := util.UnmarshalWithJSONPB(string(b), &user, false); err != nil {
110			return "", err
111		}
112		if errs := validate.CheckIstioOperatorSpec(user.Spec, false); len(errs) != 0 {
113			return "", fmt.Errorf("source spec was not valid: %v", errs)
114		}
115		base = &user
116	} else {
117		base = defaultControlPlane()
118	}
119
120	overlay, err := overlayIstioControlPlane(mesh, current, meshNetworks)
121	if err != nil {
122		return "", err
123	}
124
125	baseYAML, err := util.MarshalWithJSONPB(base)
126	if err != nil {
127		return "", err
128	}
129	overlayYAML, err := util.MarshalWithJSONPB(overlay)
130	if err != nil {
131		return "", err
132	}
133	mergedYAML, err := util.OverlayYAML(baseYAML, overlayYAML)
134	if err != nil {
135		return "", err
136	}
137
138	return mergedYAML, nil
139}
140
141func waitForReadyGateways(env Environment, mesh *Mesh) error {
142	env.Errorf("Waiting for ingress gateways to be ready\n")
143
144	var wg errgroup.Group
145
146	var notReadyMu sync.Mutex
147	notReady := make(map[string]struct{})
148
149	for uid := range mesh.clustersByClusterName {
150		c := mesh.clustersByClusterName[uid]
151		notReady[uid] = struct{}{}
152		wg.Go(func() error {
153			return env.Poll(1*time.Second, 5*time.Minute, func() (bool, error) {
154				gateways := c.readIngressGateways()
155				if len(gateways) > 0 {
156					notReadyMu.Lock()
157					delete(notReady, c.clusterName)
158					notReadyMu.Unlock()
159					return true, nil
160				}
161				return false, nil
162			})
163		})
164	}
165	if err := wg.Wait(); err != nil {
166		clusters := make([]string, 0, len(notReady))
167		for uid := range notReady {
168			clusters = append(clusters, uid)
169		}
170		return fmt.Errorf("one or more clusters gateways were not ready: %v", clusters)
171	}
172
173	env.Errorf("Ingress gateways ready\n")
174	return nil
175}
176
177func generateOutput(opt generateOptions, env Environment) error {
178	mesh, err := meshFromFileDesc(opt.filename, env)
179	if err != nil {
180		return err
181	}
182
183	if opt.waitForGateways {
184		if err := waitForReadyGateways(env, mesh); err != nil {
185			return err
186		}
187	}
188
189	context := opt.Context
190	if context == "" {
191		context = env.GetConfig().CurrentContext
192	}
193	cluster, ok := mesh.clustersByContext[context]
194	if !ok {
195		return fmt.Errorf("context %v not found", context)
196	}
197
198	meshNetwork, err := meshNetworkForCluster(env, mesh, cluster)
199	if err != nil {
200		return err
201	}
202
203	out, err := generateIstioControlPlane(mesh, cluster, meshNetwork, opt.from)
204	if err != nil {
205		return err
206	}
207	env.Printf("%v\n", out)
208
209	return nil
210}
211
212func meshNetworkForCluster(env Environment, mesh *Mesh, current *Cluster) (*v1alpha1.MeshNetworks, error) {
213	mn := &v1alpha1.MeshNetworks{
214		Networks: make(map[string]*v1alpha1.Network),
215	}
216
217	if current.DisableRegistryJoin {
218		return mn, nil
219	}
220
221	for context, cluster := range mesh.clustersByContext {
222		if _, ok := env.GetConfig().Contexts[context]; !ok {
223			return nil, fmt.Errorf("context %v not found", context)
224		}
225
226		// Don't include this cluster in the mesh's network ye t.
227		if cluster.DisableRegistryJoin {
228			continue
229		}
230
231		network := cluster.Network
232		if _, ok := mn.Networks[network]; !ok {
233			mn.Networks[network] = &v1alpha1.Network{}
234		}
235
236		for _, gateway := range cluster.readIngressGateways() {
237			// TODO debug why RegistryServiceName doesn't work
238			mn.Networks[network].Gateways = append(mn.Networks[network].Gateways,
239				&v1alpha1.Network_IstioNetworkGateway{
240					Gw: &v1alpha1.Network_IstioNetworkGateway_Address{
241						gateway.Address,
242					},
243					Port:     gateway.Port,
244					Locality: gateway.Locality,
245				},
246			)
247
248		}
249
250		// Use the cluster clusterName for the registry name so we have consistency across the mesh. Pilot
251		// uses a special name for the local cluster against which it is running.
252		registry := cluster.clusterName
253		if context == current.Context {
254			registry = string(serviceregistry.Kubernetes)
255		}
256
257		mn.Networks[network].Endpoints = append(mn.Networks[network].Endpoints,
258			&v1alpha1.Network_NetworkEndpoints{
259				Ne: &v1alpha1.Network_NetworkEndpoints_FromRegistry{
260					FromRegistry: registry,
261				},
262			},
263		)
264	}
265
266	return mn, nil
267}
268
269type generateOptions struct {
270	KubeOptions
271	filenameOption
272	from            string
273	waitForGateways bool
274}
275
276func (o *generateOptions) addFlags(flagset *pflag.FlagSet) {
277	o.filenameOption.addFlags(flagset)
278
279	flagset.StringVar(&o.from, "from", "",
280		"optional source configuration to generate multicluster aware configuration from")
281	flagset.BoolVar(&o.waitForGateways, "wait-for-gateways", false,
282		"wait for all cluster's istio-ingressgateway IPs to be ready before generating configuration.")
283}
284
285func (o *generateOptions) prepare(flags *pflag.FlagSet) error {
286	o.KubeOptions.prepare(flags)
287	return o.filenameOption.prepare()
288}
289
290func NewGenerateCommand() *cobra.Command {
291	opt := generateOptions{}
292	c := &cobra.Command{
293		Use:   "generate -f <mesh.yaml>",
294		Short: `generate a cluster-specific control plane configuration based on the mesh description and runtime state`,
295		RunE: func(c *cobra.Command, args []string) error {
296			if err := opt.prepare(c.Flags()); err != nil {
297				return err
298			}
299			env, err := NewEnvironmentFromCobra(opt.Kubeconfig, opt.Context, c)
300			if err != nil {
301				return err
302			}
303			return generateOutput(opt, env)
304		},
305	}
306	opt.addFlags(c.PersistentFlags())
307	return c
308}
309