istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/waypoint/waypoint.go (about) 1 // Copyright 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 15 package waypoint 16 17 import ( 18 "cmp" 19 "context" 20 "fmt" 21 "strings" 22 "sync" 23 "text/tabwriter" 24 "time" 25 26 "github.com/hashicorp/go-multierror" 27 "github.com/spf13/cobra" 28 "k8s.io/apimachinery/pkg/api/errors" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/types" 31 gateway "sigs.k8s.io/gateway-api/apis/v1" 32 "sigs.k8s.io/yaml" 33 34 "istio.io/api/label" 35 "istio.io/istio/istioctl/pkg/cli" 36 "istio.io/istio/pilot/pkg/model/kstatus" 37 "istio.io/istio/pkg/config/constants" 38 "istio.io/istio/pkg/config/protocol" 39 "istio.io/istio/pkg/config/schema/gvk" 40 "istio.io/istio/pkg/kube" 41 "istio.io/istio/pkg/slices" 42 "istio.io/istio/pkg/util/sets" 43 ) 44 45 var ( 46 revision = "" 47 waitReady bool 48 allNamespaces bool 49 50 deleteAll bool 51 52 trafficType = "" 53 validTrafficTypes = sets.New(constants.ServiceTraffic, constants.WorkloadTraffic, constants.AllTraffic, constants.NoTraffic) 54 55 waypointName = constants.DefaultNamespaceWaypoint 56 enrollNamespace bool 57 ) 58 59 const waitTimeout = 90 * time.Second 60 61 func Cmd(ctx cli.Context) *cobra.Command { 62 makeGateway := func(forApply bool) (*gateway.Gateway, error) { 63 ns := ctx.NamespaceOrDefault(ctx.Namespace()) 64 if ctx.Namespace() == "" && !forApply { 65 ns = "" 66 } 67 // If a user sets the waypoint name to an empty string, set it to the default namespace waypoint name. 68 if waypointName == "" { 69 waypointName = constants.DefaultNamespaceWaypoint 70 } else if waypointName == "none" { 71 return nil, fmt.Errorf("invalid name provided for waypoint, 'none' is a reserved value") 72 } 73 gw := gateway.Gateway{ 74 TypeMeta: metav1.TypeMeta{ 75 Kind: gvk.KubernetesGateway_v1.Kind, 76 APIVersion: gvk.KubernetesGateway_v1.GroupVersion(), 77 }, 78 ObjectMeta: metav1.ObjectMeta{ 79 Name: waypointName, 80 Namespace: ns, 81 }, 82 Spec: gateway.GatewaySpec{ 83 GatewayClassName: constants.WaypointGatewayClassName, 84 Listeners: []gateway.Listener{{ 85 Name: "mesh", 86 Port: 15008, 87 Protocol: gateway.ProtocolType(protocol.HBONE), 88 }}, 89 }, 90 } 91 92 // only label if the user has provided their own value, otherwise we let istiod choose a default at runtime (service) 93 // this will allow for gateway class to provide a default for that class rather than always forcing service or requiring users to configure correctly 94 if trafficType != "" { 95 if !validTrafficTypes.Contains(trafficType) { 96 return nil, fmt.Errorf("invalid traffic type: %s. Valid options are: %s", trafficType, validTrafficTypes.String()) 97 } 98 99 if gw.Labels == nil { 100 gw.Labels = map[string]string{} 101 } 102 103 gw.Labels[constants.AmbientWaypointForTrafficTypeLabel] = trafficType 104 } 105 106 if revision != "" { 107 gw.Labels = map[string]string{label.IoIstioRev.Name: revision} 108 } 109 return &gw, nil 110 } 111 waypointGenerateCmd := &cobra.Command{ 112 Use: "generate", 113 Short: "Generate a waypoint configuration", 114 Long: "Generate a waypoint configuration as YAML", 115 Example: ` # Generate a waypoint as yaml 116 istioctl x waypoint generate --namespace default`, 117 RunE: func(cmd *cobra.Command, args []string) error { 118 gw, err := makeGateway(false) 119 if err != nil { 120 return fmt.Errorf("failed to create gateway: %v", err) 121 } 122 b, err := yaml.Marshal(gw) 123 if err != nil { 124 return err 125 } 126 // strip junk 127 res := strings.ReplaceAll(string(b), ` creationTimestamp: null 128 `, "") 129 res = strings.ReplaceAll(res, `status: {} 130 `, "") 131 fmt.Fprint(cmd.OutOrStdout(), res) 132 return nil 133 }, 134 } 135 waypointGenerateCmd.PersistentFlags().StringVar(&trafficType, 136 "for", 137 "", 138 fmt.Sprintf("Specify the traffic type %s for the waypoint", sets.SortedList(validTrafficTypes)), 139 ) 140 waypointApplyCmd := &cobra.Command{ 141 Use: "apply", 142 Short: "Apply a waypoint configuration", 143 Long: "Apply a waypoint configuration to the cluster", 144 Example: ` # Apply a waypoint to the current namespace 145 istioctl x waypoint apply 146 147 # Apply a waypoint to a specific namespace and wait for it to be ready 148 istioctl x waypoint apply --namespace default --wait`, 149 RunE: func(cmd *cobra.Command, args []string) error { 150 kubeClient, err := ctx.CLIClientWithRevision(revision) 151 if err != nil { 152 return fmt.Errorf("failed to create Kubernetes client: %v", err) 153 } 154 ns := ctx.NamespaceOrDefault(ctx.Namespace()) 155 // If a user decides to enroll their namespace with a waypoint, verify that they have labeled their namespace as ambient. 156 // If they don't, the user will be warned and be presented with the command to label their namespace as ambient if they 157 // choose to do so. 158 // 159 // NOTE: This is a warning and not an error because the user may not intend to label their namespace as ambient. 160 // 161 // e.g. Users are handling ambient redirection per workload rather than at the namespace level. 162 if enrollNamespace { 163 namespaceIsLabeledAmbient, err := namespaceIsLabeledAmbient(kubeClient, ns) 164 if err != nil { 165 return fmt.Errorf("failed to check if namespace is labeled ambient: %v", err) 166 } 167 if !namespaceIsLabeledAmbient { 168 fmt.Fprintf(cmd.OutOrStdout(), "Warning: namespace is not enrolled in ambient. Consider running\t"+ 169 "`"+"kubectl label namespace %s istio.io/dataplane-mode=ambient"+"`\n", ns) 170 } 171 } 172 gw, err := makeGateway(true) 173 if err != nil { 174 return fmt.Errorf("failed to create gateway: %v", err) 175 } 176 gwc := kubeClient.GatewayAPI().GatewayV1().Gateways(ctx.NamespaceOrDefault(ctx.Namespace())) 177 b, err := yaml.Marshal(gw) 178 if err != nil { 179 return err 180 } 181 _, err = gwc.Patch(context.Background(), gw.Name, types.ApplyPatchType, b, metav1.PatchOptions{ 182 Force: nil, 183 FieldManager: "istioctl", 184 }) 185 if err != nil { 186 if errors.IsNotFound(err) { 187 return fmt.Errorf("missing Kubernetes Gateway CRDs need to be installed before applying a waypoint: %s", err) 188 } 189 return err 190 } 191 if waitReady { 192 startTime := time.Now() 193 ticker := time.NewTicker(1 * time.Second) 194 defer ticker.Stop() 195 for range ticker.C { 196 programmed := false 197 gwc, err := kubeClient.GatewayAPI().GatewayV1().Gateways(ctx.NamespaceOrDefault(ctx.Namespace())).Get(context.TODO(), gw.Name, metav1.GetOptions{}) 198 if err == nil { 199 // Check if gateway has Programmed condition set to true 200 for _, cond := range gwc.Status.Conditions { 201 if cond.Type == string(gateway.GatewayConditionProgrammed) && string(cond.Status) == "True" { 202 programmed = true 203 break 204 } 205 } 206 } 207 if programmed { 208 break 209 } 210 if time.Since(startTime) > waitTimeout { 211 errorMsg := fmt.Sprintf("timed out while waiting for waypoint %v/%v", gw.Namespace, gw.Name) 212 if err != nil { 213 errorMsg += fmt.Sprintf(": %s", err) 214 } 215 return fmt.Errorf(errorMsg) 216 } 217 } 218 } 219 fmt.Fprintf(cmd.OutOrStdout(), "waypoint %v/%v applied\n", gw.Namespace, gw.Name) 220 221 // If a user decides to enroll their namespace with a waypoint, label the namespace with the waypoint name 222 // after the waypoint has been applied. 223 if enrollNamespace { 224 err = labelNamespaceWithWaypoint(kubeClient, ns) 225 if err != nil { 226 return fmt.Errorf("failed to label namespace with waypoint: %v", err) 227 } 228 fmt.Fprintf(cmd.OutOrStdout(), "namespace %v labeled with \"%v: %v\"\n", ctx.NamespaceOrDefault(ctx.Namespace()), 229 constants.AmbientUseWaypointLabel, gw.Name) 230 } 231 return nil 232 }, 233 } 234 waypointApplyCmd.PersistentFlags().StringVar(&trafficType, 235 "for", 236 "", 237 fmt.Sprintf("Specify the traffic type %s for the waypoint", sets.SortedList(validTrafficTypes)), 238 ) 239 240 waypointApplyCmd.PersistentFlags().BoolVarP(&enrollNamespace, "enroll-namespace", "", false, 241 "If set, the namespace will be labeled with the waypoint name") 242 243 waypointDeleteCmd := &cobra.Command{ 244 Use: "delete", 245 Short: "Delete a waypoint configuration", 246 Long: "Delete a waypoint configuration from the cluster", 247 Example: ` # Delete a waypoint from the default namespace 248 istioctl x waypoint delete 249 250 # Delete a waypoint by name, which can obtain from istioctl x waypoint list 251 istioctl x waypoint delete waypoint-name --namespace default 252 253 # Delete several waypoints by name 254 istioctl x waypoint delete waypoint-name1 waypoint-name2 --namespace default 255 256 # Delete all waypoints in a specific namespace 257 istioctl x waypoint delete --all --namespace default`, 258 Args: func(cmd *cobra.Command, args []string) error { 259 if deleteAll && len(args) > 0 { 260 return fmt.Errorf("cannot specify waypoint names when deleting all waypoints") 261 } 262 if !deleteAll && len(args) == 0 { 263 return fmt.Errorf("must either specify a waypoint name or delete all using --all") 264 } 265 return nil 266 }, 267 RunE: func(cmd *cobra.Command, args []string) error { 268 kubeClient, err := ctx.CLIClient() 269 if err != nil { 270 return fmt.Errorf("failed to create Kubernetes client: %v", err) 271 } 272 ns := ctx.NamespaceOrDefault(ctx.Namespace()) 273 274 // Delete all waypoints if the --all flag is set 275 if deleteAll { 276 return deleteWaypoints(cmd, kubeClient, ns, nil) 277 } 278 279 // Delete waypoints by names if provided 280 return deleteWaypoints(cmd, kubeClient, ns, args) 281 }, 282 } 283 waypointDeleteCmd.Flags().BoolVar(&deleteAll, "all", false, "Delete all waypoints in the namespace") 284 285 waypointListCmd := &cobra.Command{ 286 Use: "list", 287 Short: "List managed waypoint configurations", 288 Long: "List managed waypoint configurations in the cluster", 289 Example: ` # List all waypoints in a specific namespace 290 istioctl x waypoint list --namespace default 291 292 # List all waypoints in the cluster 293 istioctl x waypoint list -A`, 294 RunE: func(cmd *cobra.Command, args []string) error { 295 writer := cmd.OutOrStdout() 296 kubeClient, err := ctx.CLIClient() 297 if err != nil { 298 return fmt.Errorf("failed to create Kubernetes client: %v", err) 299 } 300 var ns string 301 if allNamespaces { 302 ns = "" 303 } else { 304 ns = ctx.NamespaceOrDefault(ctx.Namespace()) 305 } 306 gws, err := kubeClient.GatewayAPI().GatewayV1().Gateways(ns). 307 List(context.Background(), metav1.ListOptions{}) 308 if err != nil { 309 return err 310 } 311 if len(gws.Items) == 0 { 312 fmt.Fprintln(writer, "No waypoints found.") 313 return nil 314 } 315 w := new(tabwriter.Writer).Init(writer, 0, 8, 5, ' ', 0) 316 slices.SortFunc(gws.Items, func(i, j gateway.Gateway) int { 317 if r := cmp.Compare(i.Namespace, j.Namespace); r != 0 { 318 return r 319 } 320 return cmp.Compare(i.Name, j.Name) 321 }) 322 filteredGws := make([]gateway.Gateway, 0) 323 for _, gw := range gws.Items { 324 if gw.Spec.GatewayClassName != constants.WaypointGatewayClassName { 325 continue 326 } 327 filteredGws = append(filteredGws, gw) 328 } 329 if allNamespaces { 330 fmt.Fprintln(w, "NAMESPACE\tNAME\tREVISION\tPROGRAMMED") 331 } else { 332 fmt.Fprintln(w, "NAME\tREVISION\tPROGRAMMED") 333 } 334 for _, gw := range filteredGws { 335 programmed := kstatus.StatusFalse 336 rev := gw.Labels[label.IoIstioRev.Name] 337 if rev == "" { 338 rev = "default" 339 } 340 for _, cond := range gw.Status.Conditions { 341 if cond.Type == string(gateway.GatewayConditionProgrammed) { 342 programmed = string(cond.Status) 343 } 344 } 345 if allNamespaces { 346 _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", gw.Namespace, gw.Name, rev, programmed) 347 } else { 348 _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", gw.Name, rev, programmed) 349 } 350 } 351 return w.Flush() 352 }, 353 } 354 waypointListCmd.PersistentFlags().BoolVarP(&allNamespaces, "all-namespaces", "A", false, "List all waypoints in all namespaces") 355 356 waypointCmd := &cobra.Command{ 357 Use: "waypoint", 358 Short: "Manage waypoint configuration", 359 Long: "A group of commands used to manage waypoint configuration", 360 Example: ` # Apply a waypoint to the current namespace 361 istioctl x waypoint apply 362 363 # Generate a waypoint as yaml 364 istioctl x waypoint generate --namespace default 365 366 # List all waypoints in a specific namespace 367 istioctl x waypoint list --namespace default`, 368 Args: func(cmd *cobra.Command, args []string) error { 369 if len(args) != 0 { 370 return fmt.Errorf("unknown subcommand %q", args[0]) 371 } 372 return nil 373 }, 374 RunE: func(cmd *cobra.Command, args []string) error { 375 cmd.HelpFunc()(cmd, args) 376 return nil 377 }, 378 } 379 380 waypointApplyCmd.PersistentFlags().StringVarP(&revision, "revision", "r", "", "The revision to label the waypoint with") 381 waypointApplyCmd.PersistentFlags().BoolVarP(&waitReady, "wait", "w", false, "Wait for the waypoint to be ready") 382 waypointCmd.AddCommand(waypointApplyCmd) 383 waypointGenerateCmd.PersistentFlags().StringVarP(&revision, "revision", "r", "", "The revision to label the waypoint with") 384 waypointCmd.AddCommand(waypointGenerateCmd) 385 waypointCmd.AddCommand(waypointDeleteCmd) 386 waypointCmd.AddCommand(waypointListCmd) 387 waypointCmd.PersistentFlags().StringVarP(&waypointName, "name", "", constants.DefaultNamespaceWaypoint, "name of the waypoint") 388 389 return waypointCmd 390 } 391 392 // deleteWaypoints handles the deletion of waypoints based on the provided names, or all if names is nil 393 func deleteWaypoints(cmd *cobra.Command, kubeClient kube.CLIClient, namespace string, names []string) error { 394 var multiErr *multierror.Error 395 if names == nil { 396 // If names is nil, delete all waypoints 397 waypoints, err := kubeClient.GatewayAPI().GatewayV1().Gateways(namespace). 398 List(context.Background(), metav1.ListOptions{}) 399 if err != nil { 400 return err 401 } 402 for _, gw := range waypoints.Items { 403 names = append(names, gw.Name) 404 } 405 } 406 407 var wg sync.WaitGroup 408 var mu sync.Mutex 409 for _, name := range names { 410 wg.Add(1) 411 go func(name string) { 412 defer wg.Done() 413 if err := kubeClient.GatewayAPI().GatewayV1().Gateways(namespace). 414 Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil { 415 if errors.IsNotFound(err) { 416 fmt.Fprintf(cmd.OutOrStdout(), "waypoint %v/%v not found\n", namespace, name) 417 } else { 418 mu.Lock() 419 multiErr = multierror.Append(multiErr, err) 420 mu.Unlock() 421 } 422 } else { 423 fmt.Fprintf(cmd.OutOrStdout(), "waypoint %v/%v deleted\n", namespace, name) 424 } 425 }(name) 426 } 427 428 wg.Wait() 429 return multiErr.ErrorOrNil() 430 } 431 432 func labelNamespaceWithWaypoint(kubeClient kube.CLIClient, ns string) error { 433 nsObj, err := kubeClient.Kube().CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}) 434 if errors.IsNotFound(err) { 435 return fmt.Errorf("namespace: %s not found", ns) 436 } else if err != nil { 437 return fmt.Errorf("failed to get namespace %s: %v", ns, err) 438 } 439 if nsObj.Labels == nil { 440 nsObj.Labels = map[string]string{} 441 } 442 nsObj.Labels[constants.AmbientUseWaypointLabel] = waypointName 443 if _, err := kubeClient.Kube().CoreV1().Namespaces().Update(context.Background(), nsObj, metav1.UpdateOptions{}); err != nil { 444 return fmt.Errorf("failed to update namespace %s: %v", ns, err) 445 } 446 return nil 447 } 448 449 func namespaceIsLabeledAmbient(kubeClient kube.CLIClient, ns string) (bool, error) { 450 nsObj, err := kubeClient.Kube().CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}) 451 if errors.IsNotFound(err) { 452 return false, fmt.Errorf("namespace: %s not found", ns) 453 } else if err != nil { 454 return false, fmt.Errorf("failed to get namespace %s: %v", ns, err) 455 } 456 if nsObj.Labels == nil { 457 return false, nil 458 } 459 return nsObj.Labels[constants.DataplaneModeLabel] == constants.DataplaneModeAmbient, nil 460 }