github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/verify/step_verify_ingress.go (about) 1 package verify 2 3 import ( 4 "fmt" 5 "net/mail" 6 "os" 7 "time" 8 9 "github.com/olli-ai/jx/v2/pkg/cmd/opts/step" 10 11 "github.com/olli-ai/jx/v2/pkg/cloud/gke" 12 "github.com/olli-ai/jx/v2/pkg/cloud/gke/externaldns" 13 "github.com/olli-ai/jx/v2/pkg/config" 14 "github.com/olli-ai/jx/v2/pkg/kube" 15 "github.com/olli-ai/jx/v2/pkg/util" 16 17 "github.com/jenkins-x/jx-logging/pkg/log" 18 "github.com/olli-ai/jx/v2/pkg/cloud" 19 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 20 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 21 "github.com/olli-ai/jx/v2/pkg/cmd/templates" 22 "github.com/pkg/errors" 23 "github.com/spf13/cobra" 24 pipelineapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/client-go/kubernetes" 27 ) 28 29 var ( 30 verifyIngressLong = templates.LongDesc(` 31 Verifies the ingress configuration defaulting the ingress domain if necessary 32 `) 33 34 verifyIngressExample = templates.Examples(` 35 # populate the ingress domain if not using a configured 'ingress.domain' setting 36 jx step verify ingress 37 38 `) 39 ) 40 41 // StepVerifyIngressOptions contains the command line flags 42 type StepVerifyIngressOptions struct { 43 step.StepOptions 44 45 Dir string 46 Namespace string 47 Provider string 48 IngressNamespace string 49 IngressService string 50 ExternalIP string 51 LazyCreate bool 52 LazyCreateFlag string 53 } 54 55 // StepVerifyIngressResults stores the generated results 56 type StepVerifyIngressResults struct { 57 Pipeline *pipelineapi.Pipeline 58 Task *pipelineapi.Task 59 PipelineRun *pipelineapi.PipelineRun 60 } 61 62 // NewCmdStepVerifyIngress Creates a new Command object 63 func NewCmdStepVerifyIngress(commonOpts *opts.CommonOptions) *cobra.Command { 64 options := &StepVerifyIngressOptions{ 65 StepOptions: step.StepOptions{ 66 CommonOptions: commonOpts, 67 }, 68 } 69 70 cmd := &cobra.Command{ 71 Use: "ingress", 72 Short: "Verifies the ingress configuration defaulting the ingress domain if necessary", 73 Long: verifyIngressLong, 74 Example: verifyIngressExample, 75 Run: func(cmd *cobra.Command, args []string) { 76 options.Cmd = cmd 77 options.Args = args 78 err := options.Run() 79 helper.CheckErr(err) 80 }, 81 } 82 83 cmd.Flags().StringVarP(&options.Dir, "dir", "d", ".", "the directory to look for the values.yaml file") 84 cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "", "the namespace to install into. Defaults to $DEPLOY_NAMESPACE if not") 85 86 cmd.Flags().StringVarP(&options.IngressNamespace, "ingress-namespace", "", opts.DefaultIngressNamesapce, "The namespace for the Ingress controller") 87 cmd.Flags().StringVarP(&options.IngressService, "ingress-service", "", opts.DefaultIngressServiceName, "The name of the Ingress controller Service") 88 cmd.Flags().StringVarP(&options.ExternalIP, "external-ip", "", "", "The external IP used to access ingress endpoints from outside the Kubernetes cluster. For bare metal on premise clusters this is often the IP of the Kubernetes master. For cloud installations this is often the external IP of the ingress LoadBalancer.") 89 cmd.Flags().StringVarP(&options.Provider, "provider", "", "", "Cloud service providing the Kubernetes cluster. Supported providers: "+cloud.KubernetesProviderOptions()) 90 cmd.Flags().StringVarP(&options.LazyCreateFlag, "lazy-create", "", "", fmt.Sprintf("Specify true/false as to whether to lazily create missing resources. If not specified it is enabled if Terraform is not specified in the %s file", config.RequirementsConfigFileName)) 91 return cmd 92 } 93 94 // Run implements this command 95 func (o *StepVerifyIngressOptions) Run() error { 96 var err error 97 if o.Dir == "" { 98 o.Dir, err = os.Getwd() 99 if err != nil { 100 return err 101 } 102 } 103 104 info := util.ColorInfo 105 ns := o.Namespace 106 if ns == "" { 107 ns = os.Getenv("DEPLOY_NAMESPACE") 108 } 109 if ns != "" { 110 if ns == "" { 111 return fmt.Errorf("no default namespace found") 112 } 113 } 114 requirements, requirementsFileName, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError) 115 if err != nil { 116 return errors.Wrapf(err, "failed to load Jenkins X requirements") 117 } 118 119 o.LazyCreate, err = requirements.IsLazyCreateSecrets(o.LazyCreateFlag) 120 if err != nil { 121 return errors.Wrapf(err, "failed to see if lazy create flag is set %s", o.LazyCreateFlag) 122 } 123 124 if requirements.Cluster.Provider == "" { 125 log.Logger().Warnf("No provider configured\n") 126 } 127 128 if requirements.Ingress.Domain == "" { 129 err = o.discoverIngressDomain(requirements, requirementsFileName) 130 if err != nil { 131 return errors.Wrapf(err, "failed to discover the Ingress domain") 132 } 133 } 134 135 // if we're using GKE and folks have provided a domain, i.e. we're not using the Jenkins X default nip.io 136 if requirements.Ingress.Domain != "" && !requirements.Ingress.IsAutoDNSDomain() && requirements.Cluster.Provider == cloud.GKE { 137 // then it may be a good idea to enable external dns and TLS 138 if !requirements.Ingress.ExternalDNS { 139 log.Logger().Info("using a custom domain and GKE, you can enable external dns and TLS") 140 } else if !requirements.Ingress.TLS.Enabled { 141 log.Logger().Info("using GKE with external dns, you can also now enable TLS") 142 } 143 144 if requirements.Ingress.ExternalDNS { 145 log.Logger().Infof("validating the external-dns secret in namespace %s\n", info(ns)) 146 147 kubeClient, err := o.KubeClient() 148 if err != nil { 149 return errors.Wrap(err, "creating kubernetes client") 150 } 151 152 cloudDNSSecretName := requirements.Ingress.CloudDNSSecretName 153 if cloudDNSSecretName == "" { 154 cloudDNSSecretName = gke.GcpServiceAccountSecretName(kube.DefaultExternalDNSReleaseName) 155 requirements.Ingress.CloudDNSSecretName = cloudDNSSecretName 156 } 157 158 err = kube.ValidateSecret(kubeClient, cloudDNSSecretName, externaldns.ServiceAccountSecretKey, ns) 159 if err != nil { 160 if o.LazyCreate { 161 log.Logger().Infof("attempting to lazily create the external-dns secret %s\n", info(ns)) 162 163 _, err = externaldns.CreateExternalDNSGCPServiceAccount(o.GCloud(), kubeClient, kube.DefaultExternalDNSReleaseName, ns, 164 requirements.Cluster.ClusterName, requirements.Cluster.ProjectID) 165 if err != nil { 166 return errors.Wrap(err, "creating the ExternalDNS GCP Service Account") 167 } 168 // lets rerun the verify step to ensure its all sorted now 169 err = kube.ValidateSecret(kubeClient, cloudDNSSecretName, externaldns.ServiceAccountSecretKey, ns) 170 } 171 } 172 if err != nil { 173 return errors.Wrap(err, "validating external-dns secret") 174 } 175 176 err = o.GCloud().EnableAPIs(requirements.Cluster.ProjectID, "dns") 177 if err != nil { 178 return errors.Wrap(err, "unable to enable 'dns' api") 179 } 180 } 181 } 182 183 // TLS uses cert-manager to ask LetsEncrypt for a signed certificate 184 if requirements.Ingress.TLS.Enabled { 185 if requirements.Cluster.Provider != cloud.GKE { 186 log.Logger().Warnf("Note that we have only tested TLS support on Google Container Engine with external-dns so far. This may not work!") 187 } 188 189 if requirements.Ingress.IsAutoDNSDomain() { 190 return fmt.Errorf("TLS is not supported with automated domains like %s, you will need to use a real domain you own", requirements.Ingress.Domain) 191 } 192 _, err = mail.ParseAddress(requirements.Ingress.TLS.Email) 193 if err != nil { 194 return errors.Wrap(err, "You must provide a valid email address to enable TLS so you can receive notifications from LetsEncrypt about your certificates") 195 } 196 } 197 198 return requirements.SaveConfig(requirementsFileName) 199 } 200 201 func (o *StepVerifyIngressOptions) discoverIngressDomain(requirements *config.RequirementsConfig, requirementsFileName string) error { 202 if requirements.Ingress.IgnoreLoadBalancer { 203 log.Logger().Infof("ignoring the load balancer to detect a public ingress domain") 204 return nil 205 } 206 client, err := o.KubeClient() 207 var domain string 208 if err != nil { 209 return errors.Wrap(err, "getting the kubernetes client") 210 } 211 212 if requirements.Ingress.Domain != "" { 213 return nil 214 } 215 216 if o.Provider == "" { 217 o.Provider = requirements.Cluster.Provider 218 if o.Provider == "" { 219 log.Logger().Warnf("No provider configured\n") 220 } 221 } 222 domain, err = o.GetDomain(client, "", 223 o.Provider, 224 o.IngressNamespace, 225 o.IngressService, 226 o.ExternalIP) 227 if err != nil { 228 return errors.Wrapf(err, "getting a domain for ingress service %s/%s", o.IngressNamespace, o.IngressService) 229 } 230 if domain == "" { 231 hasHost, err := o.waitForIngressControllerHost(client, o.IngressNamespace, o.IngressService) 232 if err != nil { 233 return errors.Wrapf(err, "getting a domain for ingress service %s/%s", o.IngressNamespace, o.IngressService) 234 } 235 if hasHost { 236 domain, err = o.GetDomain(client, "", 237 o.Provider, 238 o.IngressNamespace, 239 o.IngressService, 240 o.ExternalIP) 241 if err != nil { 242 return errors.Wrapf(err, "getting a domain for ingress service %s/%s", o.IngressNamespace, o.IngressService) 243 } 244 } else { 245 log.Logger().Warnf("could not find host for ingress service %s/%s\n", o.IngressNamespace, o.IngressService) 246 } 247 } 248 249 if domain == "" { 250 return fmt.Errorf("failed to discover domain for ingress service %s/%s", o.IngressNamespace, o.IngressService) 251 } 252 requirements.Ingress.Domain = domain 253 err = requirements.SaveConfig(requirementsFileName) 254 if err != nil { 255 return errors.Wrapf(err, "failed to save changes to file: %s", requirementsFileName) 256 } 257 log.Logger().Infof("defaulting the domain to %s and modified %s\n", util.ColorInfo(domain), util.ColorInfo(requirementsFileName)) 258 return nil 259 } 260 261 func (o *StepVerifyIngressOptions) waitForIngressControllerHost(kubeClient kubernetes.Interface, ns, serviceName string) (bool, error) { 262 loggedWait := false 263 serviceInterface := kubeClient.CoreV1().Services(ns) 264 265 if serviceName == "" || ns == "" { 266 return false, nil 267 } 268 _, err := serviceInterface.Get(serviceName, metav1.GetOptions{}) 269 if err != nil { 270 return false, err 271 } 272 273 fn := func() (bool, error) { 274 svc, err := serviceInterface.Get(serviceName, metav1.GetOptions{}) 275 if err != nil { 276 return false, err 277 } 278 279 // lets get the ingress service status 280 for _, lb := range svc.Status.LoadBalancer.Ingress { 281 if lb.Hostname != "" || lb.IP != "" { 282 return true, nil 283 } 284 } 285 286 if !loggedWait { 287 loggedWait = true 288 log.Logger().Infof("waiting for external Host on the ingress service %s in namespace %s ...", serviceName, ns) 289 } 290 return false, nil 291 } 292 err = o.RetryUntilTrueOrTimeout(time.Minute*5, time.Second*3, fn) 293 if err != nil { 294 return false, err 295 } 296 return true, nil 297 }