diff --git a/Tiltfile b/Tiltfile index eff9a358..930035c2 100644 --- a/Tiltfile +++ b/Tiltfile @@ -26,8 +26,25 @@ update_settings(k8s_upsert_timeout_secs=180) dotenv() config.define_string("aws_role", usage='AWS role to use for deployment') +config.define_string( + "keycloak_chart_source", + usage='Keycloak chart source: auto, published, or local. auto uses ../helm-charts when present and published charts otherwise.' +) +config.define_string( + "keycloak_operator_chart_version", + usage='Optional published keycloak-operator chart version. Empty uses the latest chart from the Helm repo.' +) +config.define_string( + "lifecycle_keycloak_chart_version", + usage='Optional published lifecycle-keycloak chart version. Empty uses the latest chart from the Helm repo.' +) cfg = config.parse(); aws_role = cfg.get("aws_role", "1") +keycloak_chart_source = cfg.get("keycloak_chart_source", "auto") +keycloak_operator_chart_version_override = cfg.get("keycloak_operator_chart_version", "") +lifecycle_keycloak_chart_version_override = cfg.get("lifecycle_keycloak_chart_version", "") +if keycloak_chart_source not in ["auto", "published", "local"]: + fail('keycloak_chart_source must be one of: auto, published, local') # set the aws role if aws_role: @@ -39,6 +56,18 @@ if aws_role: lifecycle_app = 'lifecycle-app' app_namespace = 'lifecycle-app' kind_cluster_name = 'lfc' +helm_charts_dir = '../helm-charts/charts' +local_keycloak_operator_chart = '{}/keycloak-operator'.format(helm_charts_dir) +local_lifecycle_keycloak_chart = '{}/lifecycle-keycloak'.format(helm_charts_dir) +lifecycle_keycloak_values = 'sysops/tilt/lifecycle-keycloak-values.yaml' +lifecycle_local_secrets = './helm/environments/local/secrets.yaml' +has_local_keycloak_charts = os.path.exists(local_keycloak_operator_chart) and os.path.exists(local_lifecycle_keycloak_chart) +use_local_keycloak_charts = keycloak_chart_source == "local" or (keycloak_chart_source == "auto" and has_local_keycloak_charts) +if keycloak_chart_source == "local" and not has_local_keycloak_charts: + fail('keycloak_chart_source=local requires ../helm-charts/charts/keycloak-operator and ../helm-charts/charts/lifecycle-keycloak') + +keycloak_operator_chart = local_keycloak_operator_chart if use_local_keycloak_charts else 'goodrxoss/keycloak-operator' +lifecycle_keycloak_chart = local_lifecycle_keycloak_chart if use_local_keycloak_charts else 'goodrxoss/lifecycle-keycloak' agent_session_workspace_image = 'lifecycle-workspace' agent_session_workspace_image_ref = '{}:latest'.format(agent_session_workspace_image) legacy_agent_session_workspace_image_ref = 'lifecycle-agent:latest' @@ -60,7 +89,7 @@ keycloak_host = ngrok_keycloak_domain or "localhost:8081" app_host = ngrok_domain or "localhost:5001" ui_host = ngrok_ui_domain or "localhost:3000" company_idp_origin = "{}://{}".format(keycloak_scheme, keycloak_host) -internal_keycloak_origin = "http://lifecycle-keycloak.{}.svc.cluster.local:8080".format(app_namespace) +internal_keycloak_origin = "http://lifecycle-keycloak-service.{}.svc.cluster.local:8080".format(app_namespace) ################################## @@ -86,6 +115,8 @@ secret_create_generic( ################################## helm_repo('bitnami', 'https://charts.bitnami.com/bitnami') helm_repo('ingress-nginx-chart', 'https://kubernetes.github.io/ingress-nginx') +if not use_local_keycloak_charts: + helm_repo('goodrxoss', 'https://goodrxoss.github.io/helm-charts') helm_resource( name='redis', @@ -185,6 +216,83 @@ k8s_resource( labels=["infra"] ) +################################## +# Keycloak Operator + Realm Chart (Helm) +################################## +keycloak_operator_deps = [] +keycloak_operator_resource_deps = [] +keycloak_operator_flags = [] +lifecycle_keycloak_deps = [ + lifecycle_keycloak_values, + lifecycle_local_secrets, +] +lifecycle_keycloak_resource_deps = [ + 'keycloak-operator', + 'legacy-keycloak-cleanup', +] +lifecycle_keycloak_flags = [] + +if use_local_keycloak_charts: + keycloak_operator_deps.append(keycloak_operator_chart) + lifecycle_keycloak_deps.append(lifecycle_keycloak_chart) + lifecycle_keycloak_resource_deps.append('lifecycle-keycloak-chart-deps') + + local_resource( + 'lifecycle-keycloak-chart-deps', + cmd='helm dependency build {}'.format(lifecycle_keycloak_chart), + deps=[ + '{}/Chart.yaml'.format(lifecycle_keycloak_chart), + '{}/values.yaml'.format(lifecycle_keycloak_chart), + ], + resource_deps=['bitnami'], + labels=['infra'], + ) +else: + if keycloak_operator_chart_version_override: + keycloak_operator_flags.extend(['--version', keycloak_operator_chart_version_override]) + keycloak_operator_resource_deps.append('goodrxoss') + if lifecycle_keycloak_chart_version_override: + lifecycle_keycloak_flags.extend(['--version', lifecycle_keycloak_chart_version_override]) + lifecycle_keycloak_resource_deps.append('goodrxoss') + +helm_resource( + name='keycloak-operator', + chart=keycloak_operator_chart, + namespace=app_namespace, + deps=keycloak_operator_deps, + resource_deps=keycloak_operator_resource_deps, + flags=keycloak_operator_flags, + labels=['infra'], +) + +local_resource( + 'legacy-keycloak-cleanup', + cmd='kubectl -n {namespace} delete deployment/lifecycle-keycloak service/lifecycle-keycloak configmap/lifecycle-keycloak-config deployment/lifecycle-keycloak-postgresql service/lifecycle-keycloak-postgresql --ignore-not-found=true'.format( + namespace=app_namespace, + ), + labels=['infra'], +) + +helm_resource( + name='lifecycle-keycloak', + chart=lifecycle_keycloak_chart, + namespace=app_namespace, + deps=lifecycle_keycloak_deps, + resource_deps=lifecycle_keycloak_resource_deps, + flags=lifecycle_keycloak_flags + [ + '-f', lifecycle_keycloak_values, + '-f', lifecycle_local_secrets, + '--set', 'hostname={}'.format(company_idp_origin), + '--set', 'clients.lifecycleUi.url={}://{}'.format(ui_scheme, ui_host), + '--set', 'companyIdp.tokenUrl={}/realms/internal/protocol/openid-connect/token'.format(internal_keycloak_origin), + '--set', 'companyIdp.authorizationUrl={}/realms/internal/protocol/openid-connect/auth'.format(company_idp_origin), + '--set', 'companyIdp.jwksUrl={}/realms/internal/protocol/openid-connect/certs'.format(internal_keycloak_origin), + '--set', 'companyIdp.logoutUrl={}/realms/internal/protocol/openid-connect/logout'.format(company_idp_origin), + '--set', 'companyIdp.issuer={}/realms/internal'.format(company_idp_origin), + ], + labels=['infra'], +) + ################################## # Worker & Web (Helm, Single Deploy) ################################## @@ -214,6 +322,7 @@ helm_set_args = [ 'namespace={}'.format(app_namespace), 'image.repository={}'.format(lifecycle_app), 'image.tag=dev', + 'keycloak.enabled=false', 'keycloak.scheme={}'.format(keycloak_scheme), 'keycloak.url={}'.format(keycloak_host), 'keycloak.appUrl={}'.format(app_host), @@ -319,22 +428,16 @@ k8s_yaml('sysops/tilt/ngrok-keycloak.yaml') k8s_resource( 'ngrok-keycloak', port_forwards=['4041:4040'], # Different local port for Keycloak ngrok admin - labels=["infra"] + labels=["infra"], + resource_deps=['lifecycle-keycloak'] ) ################################## -# Keycloak (deployed via Helm) +# Keycloak (deployed via helm-charts lifecycle-keycloak) ################################## -# Keycloak is deployed as part of the lifecycle helm release -# We just need to configure the resources for Tilt UI k8s_resource( 'lifecycle-keycloak', port_forwards=['8081:8080'], - labels=["infra"], - resource_deps=['lifecycle-keycloak-postgresql'] -) -k8s_resource( - 'lifecycle-keycloak-postgresql', labels=["infra"] ) diff --git a/helm/environments/local/lifecycle.yaml b/helm/environments/local/lifecycle.yaml index 4b445d9d..7516942b 100644 --- a/helm/environments/local/lifecycle.yaml +++ b/helm/environments/local/lifecycle.yaml @@ -230,7 +230,7 @@ keycloak: image: registry: quay.io repository: keycloak/keycloak - tag: 26.3.4 + tag: 26.4.7 resources: requests: diff --git a/helm/web-app/templates/keycloak-configmap.yaml b/helm/web-app/templates/keycloak-configmap.yaml index 5d8cc409..dadf1ae6 100644 --- a/helm/web-app/templates/keycloak-configmap.yaml +++ b/helm/web-app/templates/keycloak-configmap.yaml @@ -646,7 +646,7 @@ data: } ], "browserFlow": "browser - idp only", - "keycloakVersion": "26.3.4" + "keycloakVersion": "{{ .Values.keycloak.image.tag }}" } company-realm-config.json: |- { diff --git a/helm/web-app/templates/keycloak-deployment.yaml b/helm/web-app/templates/keycloak-deployment.yaml index e97ae8a0..8ccab989 100644 --- a/helm/web-app/templates/keycloak-deployment.yaml +++ b/helm/web-app/templates/keycloak-deployment.yaml @@ -62,7 +62,6 @@ spec: - '--hostname-strict=false' - '--health-enabled=true' - '--import-realm' - - '--proxy=edge' - '--proxy-headers=xforwarded' - '--http-enabled=true' env: diff --git a/sysops/tilt/lifecycle-keycloak-values.yaml b/sysops/tilt/lifecycle-keycloak-values.yaml new file mode 100644 index 00000000..28c37aef --- /dev/null +++ b/sysops/tilt/lifecycle-keycloak-values.yaml @@ -0,0 +1,70 @@ +# Copyright 2026 GoodRx, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +parentChartName: lifecycle + +realm: lifecycle +realmDisplayName: Lifecycle +hostname: http://localhost:8081 +hostnameStrict: false + +clients: + lifecycleUi: + url: http://localhost:3000 + +companyIdp: + enabled: true + tokenUrl: http://lifecycle-keycloak-service.lifecycle-app.svc.cluster.local:8080/realms/internal/protocol/openid-connect/token + authorizationUrl: http://localhost:8081/realms/internal/protocol/openid-connect/auth + jwksUrl: http://lifecycle-keycloak-service.lifecycle-app.svc.cluster.local:8080/realms/internal/protocol/openid-connect/certs + logoutUrl: http://localhost:8081/realms/internal/protocol/openid-connect/logout + issuer: http://localhost:8081/realms/internal + +githubIdp: + enabled: true + githubJsonFormat: true + +internalIdp: + users: + bootstrapUser: + username: lifecycle + email: lifecycle@example.com + firstName: Lifecycle + lastName: Bootstrap + password: lifecycle + credsTemp: false + +ingress: + enabled: false + tls: false + +secrets: + bootstrapAdmin: + username: admin + password: admin + postgres: + adminPassword: keycloakdb + userPassword: keycloakdb + githubIdp: + enabled: true + clientId: local-github-client-id + clientSecret: local-github-client-secret + lifecycleUi: + clientSecret: changeme + +keycloakPostgres: + nameOverride: postgres + primary: + persistence: + enabled: false diff --git a/sysops/tilt/ngrok-keycloak.yaml b/sysops/tilt/ngrok-keycloak.yaml index aced647f..bdb26db6 100644 --- a/sysops/tilt/ngrok-keycloak.yaml +++ b/sysops/tilt/ngrok-keycloak.yaml @@ -36,7 +36,7 @@ spec: - '--authtoken=$(NGROK_AUTHTOKEN)' - '--hostname=$(NGROK_KEYCLOAK_DOMAIN)' - '--log=stdout' - - 'lifecycle-keycloak:8080' # point at the Keycloak Service's name & port + - 'lifecycle-keycloak-service:8080' # operator-managed Keycloak service envFrom: - secretRef: name: ngrok-secret