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