Repository: googlecloudrobotics/core Branch: main Commit: 086c5c4d4aeb Files: 462 Total size: 3.5 MB Directory structure: gitextract_mwr70acn/ ├── .bazelignore ├── .bazelrc ├── .bazelversion ├── .dockerignore ├── .editorconfig ├── .github/ │ ├── ci/ │ │ ├── .bazelrc │ │ ├── Dockerfile.integration-test-image │ │ ├── common.sh │ │ ├── deploy_navtest.sh │ │ ├── deploy_navtest_cloudbuild.yaml │ │ ├── deployments/ │ │ │ ├── robco-integration-test/ │ │ │ │ ├── config.sh │ │ │ │ └── kubernetes/ │ │ │ │ └── k8s-relay-rollout.yaml │ │ │ └── robco-navtest/ │ │ │ └── config.sh │ │ ├── integration_test.sh │ │ ├── integration_test_cloudbuild.yaml │ │ ├── integration_test_image_builder.sh │ │ ├── presubmit.sh │ │ └── release_binary.sh │ ├── dependabot.yml │ └── workflows/ │ ├── check-bazel.yml │ ├── postsubmit.yml │ ├── presubmit.yml │ └── release.yml ├── .gitignore ├── .pep8 ├── BUILD.bazel ├── CONTRIBUTING.md ├── LICENSE ├── METADATA ├── MODULE.bazel ├── README.md ├── bazel/ │ ├── BUILD.bazel │ ├── BUILD.sysroot │ ├── app.bzl │ ├── app_chart.bzl │ ├── build_rules/ │ │ ├── app_chart/ │ │ │ ├── BUILD.bazel │ │ │ ├── Chart.yaml.template │ │ │ ├── cache_gcr_credentials.bzl │ │ │ ├── cache_gcr_credentials.sh.tpl │ │ │ ├── push_all.bzl │ │ │ ├── push_all.sh.tpl │ │ │ ├── run_parallel.bzl │ │ │ ├── run_parallel.sh.tpl │ │ │ ├── values-cloud.yaml │ │ │ └── values-robot.yaml │ │ ├── copy.bzl │ │ ├── helm_chart.bzl │ │ └── helm_template.bzl │ ├── container_push.bzl │ └── debug_repository.bzl ├── config.sh.tmpl ├── current_versions.txt ├── deploy.sh ├── docs/ │ ├── .gitignore │ ├── _config.yml │ ├── concepts/ │ │ ├── app-management.md │ │ ├── config.md │ │ ├── device_identity.md │ │ └── federation.md │ ├── developers/ │ │ └── debug-auth.md │ ├── how-to/ │ │ ├── connecting-robot.md │ │ ├── creating-declarative-api.md │ │ ├── deploy-from-sources.md │ │ ├── deploying-grpc-service.md │ │ ├── deploying-service.md │ │ ├── examples/ │ │ │ ├── charge-service/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── charge-action.yaml │ │ │ │ ├── charge-controller.yaml │ │ │ │ ├── charge-crd.yaml │ │ │ │ └── server.py │ │ │ ├── greeter-service/ │ │ │ │ ├── Makefile │ │ │ │ ├── client/ │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── client.cc │ │ │ │ ├── deploy.sh │ │ │ │ ├── greeter-server.yaml.tmpl │ │ │ │ ├── proto/ │ │ │ │ │ └── helloworld.proto │ │ │ │ └── server/ │ │ │ │ ├── Dockerfile │ │ │ │ └── server.cc │ │ │ └── hello-service/ │ │ │ ├── client/ │ │ │ │ ├── Dockerfile │ │ │ │ └── client.py │ │ │ └── server/ │ │ │ ├── Dockerfile │ │ │ ├── hello-server.yaml │ │ │ └── server.py │ │ ├── running-ros-node.md │ │ ├── setting-up-oauth.md │ │ └── using-cloud-storage.md │ ├── index.md │ ├── overview.md │ └── quickstart.md ├── new_versions.txt ├── non_module_deps.bzl ├── nvchecker.toml ├── scripts/ │ ├── BUILD.bazel │ ├── backup_robots.sh │ ├── check-images.sh │ ├── common.sh │ ├── config.sh │ ├── include-config.sh │ ├── migrate.sh │ ├── pre-commit │ ├── robot-sim.sh │ └── set-config.sh ├── src/ │ ├── .gitignore │ ├── BUILD.bazel │ ├── README.md │ ├── app_charts/ │ │ ├── BUILD.bazel │ │ ├── README.md │ │ ├── akri/ │ │ │ ├── BUILD.bazel │ │ │ ├── akri-robot.values.yaml │ │ │ ├── robot/ │ │ │ │ └── akri.yaml │ │ │ └── values-robot.yaml │ │ ├── base/ │ │ │ ├── BUILD.bazel │ │ │ ├── README.md │ │ │ ├── app_management_test.sh │ │ │ ├── cert-manager-cloud.values.yaml │ │ │ ├── cert-manager-google-cas-issuer-cloud.values.yaml │ │ │ ├── cert-manager-robot.values.yaml │ │ │ ├── cloud/ │ │ │ │ ├── app-management-policy.yaml │ │ │ │ ├── app-management.yaml │ │ │ │ ├── apps-crd.yaml │ │ │ │ ├── cert-ingress.yaml │ │ │ │ ├── cert-manager-certificates.yaml │ │ │ │ ├── cert-manager-google-cas-issuer.yaml │ │ │ │ ├── cert-manager-issuers.yaml │ │ │ │ ├── cert-manager.yaml │ │ │ │ ├── cr-syncer-auth-webhook.yaml │ │ │ │ ├── cr-syncer-policy.yaml │ │ │ │ ├── domain-redirect.yaml │ │ │ │ ├── fluentd-metrics.yaml │ │ │ │ ├── kubernetes-api.yaml │ │ │ │ ├── namespace.yaml │ │ │ │ ├── nginx-ingress-controller-policy.yaml │ │ │ │ ├── nginx-ingress-controller.yaml │ │ │ │ ├── oauth2-proxy.yaml │ │ │ │ ├── registry-crd.yaml │ │ │ │ ├── registry-policy.yaml │ │ │ │ ├── relay-dashboards.yaml │ │ │ │ ├── token-vendor-app-fwd.yaml │ │ │ │ └── token-vendor-rollout.yaml │ │ │ ├── fluent-bit-helm.sh │ │ │ ├── fluent-bit-values.yaml │ │ │ ├── relay-dashboard.json │ │ │ ├── robot/ │ │ │ │ ├── app-management.yaml │ │ │ │ ├── cert-manager-certificates.yaml │ │ │ │ ├── cert-manager-issuers.yaml │ │ │ │ ├── cert-manager.yaml │ │ │ │ ├── cr-syncer.yaml │ │ │ │ ├── fluent-bit.yaml │ │ │ │ ├── fluentd-gcp-addon.yaml │ │ │ │ ├── fluentd-metrics.yaml │ │ │ │ ├── gcr-credential-refresher.yaml │ │ │ │ └── metadata-server.yaml │ │ │ ├── values-cloud.yaml │ │ │ └── values-robot.yaml │ │ ├── k8s-relay/ │ │ │ ├── BUILD.bazel │ │ │ ├── cloud/ │ │ │ │ ├── ingress.yaml │ │ │ │ ├── kubernetes-relay-server.yaml │ │ │ │ ├── service-monitor.yaml │ │ │ │ └── service.yaml │ │ │ ├── robot/ │ │ │ │ └── kubernetes-relay-client.yaml │ │ │ ├── values-cloud.yaml │ │ │ └── values-robot.yaml │ │ ├── mission-crd/ │ │ │ ├── BUILD.bazel │ │ │ ├── mission_crd.yaml │ │ │ └── values.yaml │ │ ├── platform-apps/ │ │ │ ├── BUILD.bazel │ │ │ └── values.yaml │ │ ├── prometheus/ │ │ │ ├── BUILD.bazel │ │ │ ├── README.md │ │ │ ├── cloud/ │ │ │ │ ├── app.yaml │ │ │ │ ├── base-alerts.yaml │ │ │ │ ├── federation-service-monitor.yaml │ │ │ │ ├── grafana-ingress.yaml │ │ │ │ ├── prometheus-ingress.yaml │ │ │ │ ├── prometheus-operator.yaml │ │ │ │ ├── prometheus-relay.yaml │ │ │ │ └── storage-class.yaml │ │ │ ├── prometheus-cloud.values.yaml │ │ │ ├── prometheus-robot.values.yaml │ │ │ ├── robot/ │ │ │ │ ├── hw-exporter.yaml │ │ │ │ ├── prometheus-adapter.yaml │ │ │ │ ├── prometheus-operator.yaml │ │ │ │ ├── prometheus-relay-client.yaml │ │ │ │ └── smartctl-exporter.yaml │ │ │ ├── update_prometheus_adapter.sh │ │ │ └── values-cloud.yaml │ │ └── token-vendor/ │ │ ├── BUILD.bazel │ │ ├── cloud/ │ │ │ ├── dashboard.yaml │ │ │ ├── ingress.yaml │ │ │ ├── service-monitor.yaml │ │ │ ├── service.yaml │ │ │ ├── token-vendor-policy.yaml │ │ │ └── token-vendor.yaml │ │ └── dashboard.json │ ├── bootstrap/ │ │ ├── cloud/ │ │ │ ├── BUILD.bazel │ │ │ ├── INSTALL_FROM_BINARY │ │ │ ├── run-install.sh │ │ │ └── terraform/ │ │ │ ├── .gitignore │ │ │ ├── BUILD.bazel │ │ │ ├── README.md │ │ │ ├── address.tf │ │ │ ├── certificate-authority.tf │ │ │ ├── cluster.tf │ │ │ ├── dns.tf │ │ │ ├── endpoints.tf │ │ │ ├── gcs.tf │ │ │ ├── input.tf │ │ │ ├── logging.tf │ │ │ ├── multi-cluster-ingress.tf │ │ │ ├── output.tf │ │ │ ├── project.tf │ │ │ ├── provider.tf │ │ │ ├── registry.tf │ │ │ ├── service-account.tf │ │ │ ├── versions.tf │ │ │ ├── workload-identity.tf │ │ │ └── www.yaml │ │ └── robot/ │ │ ├── BUILD.bazel │ │ └── setup_robot.sh │ ├── go/ │ │ ├── cmd/ │ │ │ ├── app-rollout-controller/ │ │ │ │ ├── BUILD.bazel │ │ │ │ └── main.go │ │ │ ├── chart-assignment-controller/ │ │ │ │ ├── BUILD.bazel │ │ │ │ └── main.go │ │ │ ├── cr-syncer/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── health.go │ │ │ │ ├── health_test.go │ │ │ │ ├── main.go │ │ │ │ ├── main_test.go │ │ │ │ ├── syncer.go │ │ │ │ └── syncer_test.go │ │ │ ├── cr-syncer-auth-webhook/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── main.go │ │ │ │ ├── request.go │ │ │ │ └── request_test.go │ │ │ ├── gcr-credential-refresher/ │ │ │ │ ├── BUILD.bazel │ │ │ │ └── main.go │ │ │ ├── http-relay-client/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── client/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── client.go │ │ │ │ │ └── client_test.go │ │ │ │ └── main.go │ │ │ ├── http-relay-server/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── README.md │ │ │ │ ├── main.go │ │ │ │ └── server/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── broker.go │ │ │ │ ├── broker_test.go │ │ │ │ ├── server.go │ │ │ │ └── server_test.go │ │ │ ├── hw-exporter/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── metadata-server/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── coredns.go │ │ │ │ ├── coredns_test.go │ │ │ │ ├── main.go │ │ │ │ ├── main_test.go │ │ │ │ ├── metadata.go │ │ │ │ ├── metadata_test.go │ │ │ │ └── nftables.go │ │ │ ├── setup-dev/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── main.go │ │ │ │ └── setup-dev.md │ │ │ ├── setup-robot/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── synk/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── README.md │ │ │ │ └── synk.go │ │ │ └── token-vendor/ │ │ │ ├── BUILD.bazel │ │ │ ├── README.md │ │ │ ├── api/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── api.go │ │ │ │ └── v1/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── testdata/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── cloudiot/ │ │ │ │ │ │ ├── describe_device.json │ │ │ │ │ │ └── describe_device_expired_key.json │ │ │ │ │ ├── rsa_cert.pem │ │ │ │ │ └── rsa_private.pem │ │ │ │ ├── v1.go │ │ │ │ └── v1_test.go │ │ │ ├── app/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── tokenvendor.go │ │ │ │ └── tokenvendor_test.go │ │ │ ├── main.go │ │ │ ├── oauth/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── cache.go │ │ │ │ ├── cache_test.go │ │ │ │ ├── jwt/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── jwt.go │ │ │ │ │ └── jwt_test.go │ │ │ │ └── verifier.go │ │ │ ├── repository/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── k8s/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── k8s.go │ │ │ │ │ └── k8s_test.go │ │ │ │ ├── memory/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── memory.go │ │ │ │ │ └── memory_test.go │ │ │ │ └── repository.go │ │ │ ├── testdata/ │ │ │ │ ├── describe_device_a.json │ │ │ │ ├── describe_device_b.json │ │ │ │ ├── describe_device_b_blocked.json │ │ │ │ └── list_devices.json │ │ │ └── tokensource/ │ │ │ ├── BUILD.bazel │ │ │ ├── gcp.go │ │ │ └── gcp_test.go │ │ ├── generate.sh │ │ ├── pkg/ │ │ │ ├── apis/ │ │ │ │ ├── apps/ │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── register.go │ │ │ │ │ ├── types.go │ │ │ │ │ └── zz_generated.deepcopy.go │ │ │ │ └── registry/ │ │ │ │ └── v1alpha1/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── doc.go │ │ │ │ ├── register.go │ │ │ │ ├── types.go │ │ │ │ └── zz_generated.deepcopy.go │ │ │ ├── client/ │ │ │ │ ├── informers/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── apps/ │ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ │ ├── interface.go │ │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ │ ├── app.go │ │ │ │ │ │ ├── approllout.go │ │ │ │ │ │ ├── chartassignment.go │ │ │ │ │ │ ├── interface.go │ │ │ │ │ │ └── resourceset.go │ │ │ │ │ ├── factory.go │ │ │ │ │ ├── generic.go │ │ │ │ │ ├── internalinterfaces/ │ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ │ └── factory_interfaces.go │ │ │ │ │ └── registry/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── interface.go │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── interface.go │ │ │ │ │ └── robot.go │ │ │ │ ├── listers/ │ │ │ │ │ ├── apps/ │ │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ │ ├── app.go │ │ │ │ │ │ ├── approllout.go │ │ │ │ │ │ ├── chartassignment.go │ │ │ │ │ │ ├── expansion_generated.go │ │ │ │ │ │ └── resourceset.go │ │ │ │ │ └── registry/ │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── expansion_generated.go │ │ │ │ │ └── robot.go │ │ │ │ └── versioned/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── clientset.go │ │ │ │ ├── doc.go │ │ │ │ ├── fake/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── clientset_generated.go │ │ │ │ │ ├── doc.go │ │ │ │ │ └── register.go │ │ │ │ ├── scheme/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── doc.go │ │ │ │ │ └── register.go │ │ │ │ └── typed/ │ │ │ │ ├── apps/ │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── app.go │ │ │ │ │ ├── approllout.go │ │ │ │ │ ├── apps_client.go │ │ │ │ │ ├── chartassignment.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── fake/ │ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ │ ├── doc.go │ │ │ │ │ │ ├── fake_app.go │ │ │ │ │ │ ├── fake_approllout.go │ │ │ │ │ │ ├── fake_apps_client.go │ │ │ │ │ │ ├── fake_chartassignment.go │ │ │ │ │ │ └── fake_resourceset.go │ │ │ │ │ ├── generated_expansion.go │ │ │ │ │ └── resourceset.go │ │ │ │ └── registry/ │ │ │ │ └── v1alpha1/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── doc.go │ │ │ │ ├── fake/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── fake_registry_client.go │ │ │ │ │ └── fake_robot.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── registry_client.go │ │ │ │ └── robot.go │ │ │ ├── configutil/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── config_reader.go │ │ │ │ └── config_reader_test.go │ │ │ ├── controller/ │ │ │ │ ├── approllout/ │ │ │ │ │ ├── BUILD.bazel │ │ │ │ │ ├── controller.go │ │ │ │ │ └── controller_test.go │ │ │ │ └── chartassignment/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── controller.go │ │ │ │ ├── release.go │ │ │ │ ├── release_test.go │ │ │ │ ├── validator.go │ │ │ │ └── validator_test.go │ │ │ ├── gcr/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── update_gcr_credential_test.go │ │ │ │ └── update_gcr_credentials.go │ │ │ ├── kubetest/ │ │ │ │ ├── BUILD.bazel │ │ │ │ └── kubetest.go │ │ │ ├── kubeutils/ │ │ │ │ ├── BUILD.bazel │ │ │ │ └── kubeutils.go │ │ │ ├── robotauth/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── robotauth.go │ │ │ │ └── robotauth_test.go │ │ │ ├── setup/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── setupcommon.go │ │ │ │ ├── setupcommon_test.go │ │ │ │ └── util/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── factory.go │ │ │ │ └── fake.go │ │ │ └── synk/ │ │ │ ├── BUILD.bazel │ │ │ ├── interface.go │ │ │ ├── sort.go │ │ │ ├── sort_test.go │ │ │ ├── synk.go │ │ │ └── synk_test.go │ │ └── tests/ │ │ ├── BUILD.bazel │ │ ├── apps/ │ │ │ ├── BUILD.bazel │ │ │ ├── apps_test.go │ │ │ └── run.sh │ │ ├── k8s_integration_test.go │ │ ├── k8s_integration_test_auth_helper.go │ │ ├── relay/ │ │ │ ├── BUILD.bazel │ │ │ ├── in_process_relay_test.go │ │ │ └── nok8s_relay_test.go │ │ ├── relay-bench.sh │ │ └── relay_test.sh │ ├── go.mod │ ├── go.sum │ ├── gomod.sh │ └── proto/ │ └── http-relay/ │ ├── BUILD.bazel │ ├── http_over_rpc.proto │ └── unused.go └── third_party/ ├── BUILD ├── BUILD.bazel ├── README.md ├── akri/ │ ├── BUILD.bazel │ ├── akri-0.12.9.tgz │ ├── akri-configuration-crd.yaml │ ├── akri-instance-crd.yaml │ └── update-akri.sh ├── app_crd.BUILD ├── cert-manager/ │ ├── BUILD.bazel │ └── cert-manager-v1.16.3.tgz ├── cert-manager-google-cas-issuer/ │ ├── BUILD.bazel │ └── cert-manager-google-cas-issuer-v0.6.2.tgz ├── fluentd_gcp_addon/ │ ├── BUILD.bazel │ ├── fluentd-gcp-configmap.yaml │ └── fluentd-gcp-ds.yaml ├── helm2/ │ └── BUILD.bazel ├── helm3/ │ └── BUILD.bazel ├── ingress-nginx.BUILD ├── kube-prometheus-stack/ │ ├── 00-crds.yaml │ ├── 01-crds.yaml │ ├── BUILD.bazel │ ├── kube-prometheus-stack-72.9.1.tgz │ └── update_crd.sh ├── kubernetes_proto/ │ ├── meta/ │ │ ├── BUILD.bazel │ │ ├── README.md │ │ └── generated.proto │ ├── runtime/ │ │ ├── BUILD.bazel │ │ └── generated.proto │ └── schema/ │ ├── BUILD.bazel │ └── generated.proto └── terraform.BUILD ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bazelignore ================================================ src/.gopath ================================================ FILE: .bazelrc ================================================ # Enable Bzlmod for every Bazel command common --enable_bzlmod # Work around go issue with LLVM 15+: https://github.com/bazelbuild/rules_go/issues/3691#issuecomment-2263999685 build --@io_bazel_rules_go//go/config:linkmode=pie # Enforce stricter environment rules, which eliminates some non-hermetic # behavior and therefore improves both the remote_cache cache hit rate and the # correctness and repeatability of the build. build --incompatible_strict_action_env=true # Make sure that no regressions are introduced until the flag is flipped # See: https://github.com/bazelbuild/bazel/issues/8195 build --incompatible_disallow_empty_glob # Use the new paths. # https://github.com/bazelbuild/bazel/issues/23127 common --incompatible_use_plus_in_repo_names # Always use the pre-configured toolchain. build --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 # Set a higher timeout value, just in case. build --remote_timeout=3600 # Platform flags # The toolchain container used for execution is defined in the target indicated # by "extra_execution_platforms", "host_platform" and "platforms". # More about platforms: https://docs.bazel.build/versions/master/platforms.html build:linux_x86_64 --extra_execution_platforms=//bazel:linux_x86_64 build:linux_x86_64 --host_platform=//bazel:linux_x86_64 build:linux_x86_64 --platforms=//bazel:linux_x86_64 build:remote --remote_executor=grpcs://remotebuildexecution.googleapis.com build:remote_cache --remote_cache=grpcs://remotebuildexecution.googleapis.com # Enable authentication. This will pick up application default credentials by # default. You can use --google_credentials=some_file.json to use a service # account credential instead. build:remote --google_default_credentials=true build:remote_cache --google_default_credentials=true # RBE builds only support linux_x86_64. build:remote --config=linux_x86_64 build:remote_cache --config=linux_x86_64 # Don't run integration tests and tests that need docker by default test --test_tag_filters="-external,-requires-docker" ================================================ FILE: .bazelversion ================================================ 8.4.2 ================================================ FILE: .dockerignore ================================================ * ================================================ FILE: .editorconfig ================================================ # Editor configuration, see http://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.go] indent_style = tab indent_size = 8 [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: .github/ci/.bazelrc ================================================ # Bazel config for CI/CD builds. # Default to keep going build --keep_going # Use rbe remote execution and caching on robco-integration-test. build --config=remote build --remote_instance_name=projects/robco-integration-test/instances/default_instance build --google_default_credentials=true # Slightly higher than the numer of available remote workers (10 in default_instance). # This has not been tuned a lot. build --jobs=12 # No neeed to download every intermediate output to the local runner. build --remote_download_toplevel # Use Result Store to store Build and Test logs . build --bes_backend=buildeventservice.googleapis.com build --bes_results_url=https://source.cloud.google.com/results/invocations build --bes_timeout=600s build --bes_instance_name=robco-integration-test # Try to mitigate DEADLINE_EXCEEDED errors (b/346715839). # Remove experimental_ prefix when updating Bazel. build --experimental_build_event_upload_max_retries=8 ================================================ FILE: .github/ci/Dockerfile.integration-test-image ================================================ # Image used for integration_test.sh on Cloud Build. # Allows access to GKE and to run Bazel commands. FROM gcr.io/cloud-builders/kubectl # Install Bazelisk RUN \ VERSION="v1.21.0" && \ curl -L https://github.com/bazelbuild/bazelisk/releases/download/${VERSION}/bazelisk-linux-amd64 --output /usr/bin/bazelisk && \ chmod +x /usr/bin/bazelisk && \ ln -s /usr/bin/bazelisk /usr/bin/bazel RUN mkdir -p /builder /output /workspace && chmod -R 777 /output # rules_python is not happy if bazel runs as root so create a new user # https://github.com/bazelbuild/rules_python/pull/713 # https://github.com/GoogleCloudPlatform/cloud-builders/issues/641 RUN adduser builder --disabled-password # Allow running sudo without password # Add libtinfo5, which is required locally until we can upgrade to LLVM 19 RUN apt-get update && apt-get install -y sudo libtinfo5 && apt-get clean && rm -rf /var/lib/apt/lists/* && \ usermod -aG sudo builder && \ echo "builder ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/builder" && chmod 440 "/etc/sudoers.d/builder" # For some reason //src/go/tests:go_default_test is expecting # the kubeconfig in /home/builder/.kube/config, i.e. it does not use $HOME # (which is /builder/home). alexanderfaxa@ could not figure out why so just # copy the config there. RUN mkdir -p /home/builder/.kube && \ ln -s /builder/home/.kube/config /home/builder/.kube/config USER builder ================================================ FILE: .github/ci/common.sh ================================================ #!/bin/bash # Format for the xtrace lines export 'PS4=+$(date --rfc-3339=seconds):${BASH_SOURCE}:${LINENO}: ' set -o errexit # exit immediately, if a pipeline command fails set -o pipefail # returns the last command to exit with a non-zero status set -o xtrace # print command traces before executing command RUNFILES_ROOT="_main" # Wraps the common Bazel flags for CI for brevity. function bazel_ci { bazelisk --bazelrc="${DIR}/.bazelrc" "$@" } function generate_build_id() { # Considerations for a build identifier: It must be unique, it shouldn't break # if we try multiple dailies in a day, and it would be nice if a textual sort # would put newest releases last. git_hash=$(echo "$GITHUB_SHA" | cut -c1-6) date "+daily-%Y-%m-%d-${git_hash}" } # Pushes images and releases a binary to a specified bucket. # bucket: target GCS bucket to release to # name: name of the release tar ball # labels: optional list of filename aliases for the release, these are one-line # text files with the release name as a bucket local path function release_binary { local bucket="$1" local name="$2" # This function is called from test and release pipelines. We (re)build the binary and push the # app images here to ensure the app images which are referenced in the binary exist in the # registry. bazel_ci build \ //src/bootstrap/cloud:crc-binary \ //src/app_charts:push \ //src/go/cmd/setup-robot:setup-robot.push # The push scripts depends on binaries in the runfiles. local oldPwd oldPwd=$(pwd) # The tag variable must be called 'TAG', see cloud-robotics/bazel/container_push.bzl for t in latest ${DOCKER_TAG}; do cd ${oldPwd}/bazel-bin/src/go/cmd/setup-robot/push_setup-robot.push.sh.runfiles/${RUNFILES_ROOT} ${oldPwd}/bazel-bin/src/go/cmd/setup-robot/push_setup-robot.push.sh \ --repository="${CLOUD_ROBOTICS_CONTAINER_REGISTRY}/setup-robot" \ --tag="${t}" cd ${oldPwd}/bazel-bin/src/app_charts/push.runfiles/${RUNFILES_ROOT} TAG="$t" ${oldPwd}/bazel-bin/src/app_charts/push "${CLOUD_ROBOTICS_CONTAINER_REGISTRY}" done cd ${oldPwd} gcloud storage cp \ --predefined-acl=publicRead \ bazel-bin/src/bootstrap/cloud/crc-binary.tar.gz \ "gs://${bucket}/${name}.tar.gz" # Overwrite cache control as we want changes to run-install.sh and version files to be visible # right away. gcloud storage cp \ --predefined-acl=publicRead \ --cache-control="private, max-age=0, no-transform" \ src/bootstrap/cloud/run-install.sh \ "gs://${bucket}/" # The remaining arguments are version labels. GCS does not support symlinks, so we use version # files instead. local vfile vfile=$(mktemp) echo "${name}.tar.gz" >${vfile} shift 2 # Loop over remianing args in $* and creat alias files. for label; do gcloud storage cp \ --predefined-acl=publicRead \ --cache-control="private, max-age=0, no-transform" \ ${vfile} "gs://${bucket}/${label}" done } ================================================ FILE: .github/ci/deploy_navtest.sh ================================================ #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/common.sh" PROJECT_DIR="${DIR}/deployments/robco-navtest" source "${PROJECT_DIR}/config.sh" # TODO(skopecki) These variables should be declared in the run-install.sh and removed from this script. export BUCKET_URI="https://storage.googleapis.com/robco-ci-binary-builds" export SOURCE_CONTAINER_REGISTRY="gcr.io/robco-team" # Deploy the binary release that was pushed by the last successful integration test. curl --silent --show-error --fail "${BUCKET_URI}/run-install.sh" \ | bash -x -s -- ${GCP_PROJECT_ID} ================================================ FILE: .github/ci/deploy_navtest_cloudbuild.yaml ================================================ # Call deploy_navtest.sh on Cloud Build. # TODO(b/323509860): Run directly on the Action runner when it supports WIF. steps: - name: "gcr.io/cloud-builders/gcloud" entrypoint: "bash" args: ["./.github/ci/deploy_navtest.sh"] timeout: 1200s ================================================ FILE: .github/ci/deployments/robco-integration-test/config.sh ================================================ #!/usr/bin/env bash # Enable cloud robotics layer 2 APP_MANAGEMENT=true GCP_PROJECT_ID=robco-integration-test GCP_REGION=europe-west1 GCP_ZONE=europe-west1-c CLOUD_ROBOTICS_SHARED_OWNER_GROUP=cloud-robotics-cloud-owner-acl@twosync.google.com CLOUD_ROBOTICS_DEPLOY_ENVIRONMENT=GCP-testing TERRAFORM_GCS_BUCKET="robco-team-terraform-state" TERRAFORM_GCS_PREFIX="state/${GCP_PROJECT_ID}" CLOUD_ROBOTICS_CONTAINER_REGISTRY=gcr.io/robco-team PRIVATE_DOCKER_PROJECTS=robco-team CLOUD_ROBOTICS_CTX=gke_robco-integration-test_europe-west1-c_cloud-robotics ================================================ FILE: .github/ci/deployments/robco-integration-test/kubernetes/k8s-relay-rollout.yaml ================================================ apiVersion: apps.cloudrobotics.com/v1alpha1 kind: AppRollout metadata: name: k8s-relay labels: app: k8s-relay spec: appName: k8s-relay-dev cloud: {} robots: - selector: any: true ================================================ FILE: .github/ci/deployments/robco-navtest/config.sh ================================================ #!/usr/bin/env bash # Enable google cloud robotics layer 2 APP_MANAGEMENT=true GCP_PROJECT_ID=robco-navtest GCP_REGION=europe-west1 GCP_ZONE=europe-west1-c CLOUD_ROBOTICS_SHARED_OWNER_GROUP=cloud-robotics-cloud-owner-acl@twosync.google.com TERRAFORM_GCS_BUCKET="robco-team-terraform-state" TERRAFORM_GCS_PREFIX="state/${GCP_PROJECT_ID}" CLOUD_ROBOTICS_CONTAINER_REGISTRY=gcr.io/robco-team PRIVATE_DOCKER_PROJECTS=robco-team CLOUD_ROBOTICS_CTX=gke_robco-navtest_europe-west1-c_cloud-robotics ================================================ FILE: .github/ci/integration_test.sh ================================================ #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/common.sh" source "./scripts/common.sh" # Because the format from common.sh is not recognized by Cloud Build. export 'PS4=' # Need to source the project config from here PROJECT_DIR="${DIR}/deployments/robco-integration-test" source "${PROJECT_DIR}/config.sh" gcloud config set project ${GCP_PROJECT_ID} gke_get_credentials "${GCP_PROJECT_ID}" "cloud-robotics" "${GCP_REGION}" "${GCP_ZONE}" BUILD_IDENTIFIER=$(generate_build_id) echo "INFO: Build identifier is $BUILD_IDENTIFIER" export BAZEL_FLAGS="--bazelrc=${DIR}/.bazelrc" bash -x ./deploy.sh update "${GCP_PROJECT_ID}" # Create a GKE cluster with a single robot for the relay test. ROBOT_RELAY_CLUSTER="relay-test" export SKIP_LOCAL_PULL=true bash -x ./scripts/robot-sim.sh create "${GCP_PROJECT_ID}" "${ROBOT_RELAY_CLUSTER}" bazel_ci run //src/go/cmd/setup-dev -- --project="${GCP_PROJECT_ID}" --robot-name="${ROBOT_RELAY_CLUSTER}" DOMAIN=${CLOUD_ROBOTICS_DOMAIN:-"www.endpoints.${GCP_PROJECT_ID}.cloud.goog"} ROBOT_CONTEXT="gke_${GCP_PROJECT_ID}_${GCP_ZONE}_${ROBOT_RELAY_CLUSTER}" # Output state of cloud and robot k8s context to inspect the health of pods. kubectl config get-contexts || true kubectl --context ${CLOUD_ROBOTICS_CTX} get pods || true kubectl --context ${GCP_PROJECT_ID}-robot get pods || true kubectl --context ${ROBOT_CONTEXT} get pods || true bazel_ci test \ --test_env GCP_PROJECT_ID=${GCP_PROJECT_ID} \ --test_env GCP_REGION=${GCP_REGION} \ --test_env GCP_ZONE=${GCP_ZONE} \ --test_env CLUSTER=${ROBOT_RELAY_CLUSTER} \ --test_env PATH=$PATH \ --jvmopt="-DCLOUD_ROBOTICS_DOMAIN=${DOMAIN}" \ --test_output=streamed \ --test_tag_filters="external" \ --strategy=TestRunner=standalone \ //... # If this is running on main (ie, not a manual run) then update the `latest` # binary. if [[ "$MANUAL_RUN" == "false" ]] ; then release_binary "robco-ci-binary-builds" "crc-${BUILD_IDENTIFIER}" "latest" fi ================================================ FILE: .github/ci/integration_test_cloudbuild.yaml ================================================ # A Cloud Build job for running integration_test.sh. # TODO(b/323509860): Run directly on the Action runner when it supports WIF. steps: # Needed for cloud build to allow running Bazel as non-root, see # https://github.com/GoogleCloudPlatform/cloud-builders/issues/641#issuecomment-604599102 # Not part of the Dockerfile since the chmod layer adds significant image size. - name: ubuntu entrypoint: "bash" args: ["-c", "chmod -R 777 /builder && chmod -R 777 /workspace"] # This runs on a custom image that has kubectl, gcloud and bazel installed. # See Dockerfile.integration-test-image. - name: "gcr.io/robco-integration-test/integration-test-image@sha256:87e7cde1d2923eed014ee8ee0c365d683fa7a8a99985bbc77acb951c2e6faefc" entrypoint: "bash" args: ["./.github/ci/integration_test.sh"] env: - "GITHUB_SHA=${_GITHUB_SHA}" - "MANUAL_RUN=${_MANUAL_RUN}" substitutions: _GITHUB_SHA: "" _MANUAL_RUN: "" options: dynamicSubstitutions: true substitutionOption: "MUST_MATCH" timeout: 1800s ================================================ FILE: .github/ci/integration_test_image_builder.sh ================================================ #!/bin/bash # Builds and pushes a docker image that can be used in Cloud Build to run # the integration test (see integration_test_cloudbuild.yaml). # # To be manually invoked and the resulting sha256 copied to # integration_test_cloudbuild.yaml after changing the Dockerfile. set -euo pipefail NAME="gcr.io/robco-integration-test/integration-test-image" docker build --network=host -t "${NAME}" - \ < .github/ci/Dockerfile.integration-test-image docker push "${NAME}" ================================================ FILE: .github/ci/presubmit.sh ================================================ #!/bin/bash # # Presubmit script for testing cloud robotics. # Expected to run remotely on a GitHub Actions runner, not locally. DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=ci/common.sh source "${DIR}/common.sh" echo "Timestamp: build started" bazel_ci build --nobuild //... echo "Timestamp: build-deps fetched" bazel_ci build //... echo "Timestamp: build done" bazel_ci test --test_output=errors //... echo "Timestamp: test done" # Some of the tests below pull Docker images from the repository. We need to # make sure they are pushed and provide an access token. gcloud auth configure-docker --quiet REGISTRY="gcr.io/robco-integration-test" TAG="latest" bazel_ci run \ //src/app_charts:push "${REGISTRY}" # We're running into timeouts at CI and also don't see the actual failure # reasons. Disable the test for now until someone has time and ideas how to # resurrect it. # # set +o xtrace # Don't put the access token in the logs. # ACCESS_TOKEN="$(gcloud auth application-default print-access-token)" # Note: --strategy=TestRunner=standalone means that the tests are run locally # and not on a remote worker (which does not have the Docker environment). # bazel_ci test \ # --flaky_test_attempts 3 \ # --test_env ACCESS_TOKEN="${ACCESS_TOKEN}" \ # --test_env REGISTRY="${REGISTRY}" \ # --test_tag_filters="requires-docker" \ # --test_output=errors \ # --strategy=TestRunner=standalone //src/go/tests/apps:go_default_test # # set -o xtrace echo "Timestamp: presubmit.sh done" ================================================ FILE: .github/ci/release_binary.sh ================================================ #!/bin/bash set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=ci/common.sh source "${DIR}/common.sh" gcloud auth configure-docker --quiet # Set defaults used for the release, they can only be overriden when testing # manually. GCP_BUCKET=${GCP_BUCKET:-"cloud-robotics-releases"} VERSION=${VERSION:-"0.1.0"} SHA=$(git rev-parse --short "$FULL_SHA") RELEASE_NAME="v$VERSION-$SHA" # TAG is a global variable that is used in the container push rules. export TAG="crc-${VERSION}-${SHA}" LABELS=${LABELS:-"latest crc-${VERSION}/crc-${VERSION}+latest"} # Get the last release. We only create a new release if the main branch has moved since # as trying to re-create an existing release is an error. output=$(curl --fail-with-body -sS \ -H "Accept: application/vnd.github+json" \ -H "Authorization: token $GITHUB_TOKEN" \ https://api.github.com/repos/$REPO/releases/latest) PREVIOUS_RELEASE_NAME="$(jq -r '.tag_name' <<< $output)" if [ "$RELEASE_NAME" = "$PREVIOUS_RELEASE_NAME" ]; then echo "Release $RELEASE_NAME already exists. Nothing more to do." exit 0 else echo "Previous release is $PREVIOUS_RELEASE_NAME" fi CLOUD_ROBOTICS_CONTAINER_REGISTRY="gcr.io/cloud-robotics-releases" # DOCKER_TAG is a global variable that is used in release_binary. DOCKER_TAG=${DOCKER_TAG:-"crc-${VERSION}-${SHA}"} release_binary "${GCP_BUCKET}" "crc-${VERSION}/crc-${VERSION}+${SHA}" ${LABELS} # Generate release notes comparing against the previous release. output=$(curl --fail-with-body -sS \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: token $GITHUB_TOKEN" \ https://api.github.com/repos/$REPO/releases/generate-notes \ -d '{"tag_name":"'$RELEASE_NAME'","previous_tag_name":"'$PREVIOUS_RELEASE_NAME'"}') # Code newlines as literal \n and escape double quotes to generate valid JSON. BODY="$(jq -r '.body' <<< $output | awk '{printf "%s\\n", $0}' | sed 's/"/\\"/g')" echo "Generated release notes for $RELEASE_NAME" # Create the release on GitHub. curl --fail-with-body -sS \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: token $GITHUB_TOKEN" \ https://api.github.com/repos/$REPO/releases \ --data-binary @- << EOF { "tag_name": "$RELEASE_NAME", "name": "$RELEASE_NAME", "body": "$BODY" } EOF ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/src/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "bazel" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/check-bazel.yml ================================================ name: Check Bazel on: workflow_call: permissions: contents: read id-token: write jobs: check-bazel: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 - name: Auth uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # tag=v3.0.0 with: create_credentials_file: true # also sets GOOGLE_APPLICATION_CREDENTIALS service_account: "github-automation-bot@gha-crc-dev.iam.gserviceaccount.com" workload_identity_provider: "projects/1043719249528/locations/global/workloadIdentityPools/github-automation/providers/crc-dev" - name: Print error on auth fail if: failure() run: | echo >&2 "This PR appears to be from a fork or authored by a non-org member, rather than from the primary repo." echo >&2 "This means it can't run the presubmit, which requires access to GCR." echo >&2 "If you are a project member, please push your branch to github.com/googlecloudrobotics/core instead." exit 1 - name: Run .github/ci/presubmit.sh run: ./.github/ci/presubmit.sh - name: Get bazel server logs if: success() || failure() run: cat ~/.cache/bazel/_bazel_*/*/java.log || true ================================================ FILE: .github/workflows/postsubmit.yml ================================================ name: Postsubmit on: schedule: - cron: "0 4 * * *" # Once a day at 4am. # Manual runs through Actions tab in the UI workflow_dispatch: inputs: force-binary-release: description: >- force-binary-release: Set to non-empty when running from main to create a binary release that can be used by 'Create release'. permissions: contents: read id-token: write concurrency: group: integration_test cancel-in-progress: true jobs: call-bazel: uses: ./.github/workflows/check-bazel.yml integration-test: runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 - name: Auth uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # tag=v3.0.0 with: create_credentials_file: true # also sets GOOGLE_APPLICATION_CREDENTIALS service_account: "github-automation-bot@gha-crc-dev.iam.gserviceaccount.com" workload_identity_provider: "projects/1043719249528/locations/global/workloadIdentityPools/github-automation/providers/crc-dev" - name: Run integration_test.sh on Cloud Build env: MANUAL_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.force-binary-release == '' }}" run: | gcloud builds submit \ --project robco-integration-test \ --region europe-west1 \ --config .github/ci/integration_test_cloudbuild.yaml \ --substitutions _GITHUB_SHA=${GITHUB_SHA},_MANUAL_RUN=${MANUAL_RUN} ================================================ FILE: .github/workflows/presubmit.yml ================================================ name: Presubmit on: pull_request: branches: ["main"] workflow_dispatch: permissions: contents: read id-token: write pull-requests: read # Cancel previous runs if a new one is started. concurrency: group: ${{ github.ref }} cancel-in-progress: true jobs: setup-presubmit: runs-on: ubuntu-22.04 outputs: presubmit_digest: ${{ steps.pr-digest.outputs.digest }} presubmit_status: ${{ steps.status.outputs.status }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 - name: Get PR digest id: pr-digest env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail # Get the list of changed files in the PR. gh pr view ${{github.event.number}} --json files -q '.files[].path' > /tmp/changed_files.txt # Create a tarball of the changed files and compute its SHA256. # --ignore-failed-read to not fail on deleted files. tar -cvf /tmp/changed_files.tar \ --owner=root --group=root --numeric-owner --mtime="2010-01-01" --sort=name \ -T /tmp/changed_files.txt \ --ignore-failed-read digest=$(cat /tmp/changed_files.txt /tmp/changed_files.tar | sha256sum | cut -d " " -f1) echo "digest=$digest" >> $GITHUB_OUTPUT - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/PRESUBMITS_SUCCEEDED key: PRESUBMITS_SUCCEEDED-${{ steps.pr-digest.outputs.digest }} - name: Check for previous runs id: status run: | if [ -f ~/PRESUBMITS_SUCCEEDED ]; then echo "status=success" >> $GITHUB_OUTPUT fi call-check-bazel: needs: [setup-presubmit] if: ${{ needs.setup-presubmit.outputs.presubmit_status != 'success' }} uses: ./.github/workflows/check-bazel.yml presubmits-ok: needs: [setup-presubmit, call-check-bazel] runs-on: ubuntu-22.04 # To ensure this job always runs even if the "heavy" jobs were skipped. # This allows us to guard merging on this check in Branch Protection. if: ${{ always() }} steps: - name: Fail if tests failed # always() because GitHub requires a status macro to be included or else this gets skipped. # https://github.com/actions/runner/issues/491#issuecomment-850884422 if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} run: exit 1 - name: Create presubmits succeeded marker run: touch ~/PRESUBMITS_SUCCEEDED - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/PRESUBMITS_SUCCEEDED key: PRESUBMITS_SUCCEEDED-${{ needs.setup-presubmit.outputs.presubmit_digest }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Create release on: schedule: - cron: "0 5 * * *" # Once a day at 5am. # Manual runs through Actions tab in the UI workflow_dispatch: permissions: actions: read contents: write id-token: write pull-requests: read # Cancel previous runs if a new one is started. concurrency: group: ${{ github.ref }} cancel-in-progress: true jobs: create_release: runs-on: ubuntu-22.04 steps: # Check out repo at latest green postsubmit commit on the main branch. - name: Get latest passing commit id: latest-green env: REPO: ${{ github.repository }} run: | set -euo pipefail output=$(curl --fail-with-body -sS \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPO/actions/workflows/postsubmit.yml/runs?per_page=1&branch=main&event=schedule&status=success") repo_id=$(jq -r '.workflow_runs[0].head_repository.id' <<< $output) if [[ "${repo_id}" != "${{ github.repository_id }}" ]] ; then echo >&2 "Unexpected head repository ID: ${repo_id} - check postsubmit.yml configuration" exit 1 fi sha=$(jq -r '.workflow_runs[0].head_sha' <<< $output) echo "latest_green=$sha" >> $GITHUB_OUTPUT - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 with: ref: ${{ steps.latest-green.outputs.latest_green }} - name: Auth uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # tag=v3.0.0 with: create_credentials_file: true # also sets GOOGLE_APPLICATION_CREDENTIALS service_account: "github-automation-bot@gha-crc-prod.iam.gserviceaccount.com" workload_identity_provider: "projects/695270090783/locations/global/workloadIdentityPools/github-automation/providers/crc-prod" - name: "Set up gcloud" uses: "google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db" # tag=v3.0.1 with: skip_install: true - name: Deploy Navtest on Cloud Build run: | gcloud builds submit \ --project robco-navtest \ --config .github/ci/deploy_navtest_cloudbuild.yaml # Now we are ready to create the release. - name: Run release_binary.sh env: REPO: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FULL_SHA: ${{ steps.latest-green.outputs.latest_green }} run: ./.github/ci/release_binary.sh ================================================ FILE: .gitignore ================================================ .cache/ *.pyc env/ # auto-generated by scripts/backport_kubeadm.sh nsenter # IntelliJ files .project/ # Bazel bazel-* buildprofile.out buildprofile.out.html ================================================ FILE: .pep8 ================================================ [pep8] aggressive=2 indent-size=2 max-line-length=80 ================================================ FILE: BUILD.bazel ================================================ # Description: # Root BUILD file for cloud-robotics load("@bazel_gazelle//:def.bzl", "gazelle") package(default_visibility = ["//visibility:public"]) exports_files([ "config.sh.tmpl", "deploy.sh", ]) # Gazelle uses this to build importpath attributes. # gazelle:prefix github.com/googlecloudrobotics/core # Gazelle is used to generate BUILD.bazel files for WORKSPACE dependencies # running this manually via "bazel run //:gazelle" will regenerate BUILD.bazel files that # contain go-rules. gazelle( name = "gazelle", ) # Libraries are named go_default_library, tests are named go_default_test. # gazelle:go_naming_convention go_default_library # We ignore the build files generated by bazel-deps as it doesn't use buildifer. # gazelle:exclude third_party # Also ignore the Go sources downloaded by src/go/deps.sh. # gazelle:exclude src/.gopath # Don't created build files for these examples # gazelle:exclude docs/how-to/examples/greeter-service/proto/ ================================================ FILE: CONTRIBUTING.md ================================================ # How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Code formatting We have a pre-commit hook to check code formatting, which you can install with: ``` ln -s ../../scripts/pre-commit .git/hooks/ ``` It depends on external tools for formatting, which you may be prompted to install when it first runs. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: METADATA ================================================ name: "Cloud Robotics" third_party { # https://nvd.nist.gov/products/cpe/search security { tag: "NVD-CPE2.3:cpe:/a:grafana:grafana:10.3.1" tag: "NVD-CPE2.3:cpe:/a:kubernetes:ingress-nginx:1.8.4" tag: "NVD-CPE2.3:cpe:/a:oauth2_proxy_project:oauth2_proxy:7.5.1" tag: "NVD-CPE2.3:cpe:/a:prometheus:prometheus:2.49.1" } } ================================================ FILE: MODULE.bazel ================================================ bazel_dep(name = "aspect_bazel_lib", version = "2.21.2") bazel_dep(name = "bazel_skylib", version = "1.8.1") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "protobuf", version = "29.0", repo_name = "com_google_protobuf") bazel_dep(name = "rules_oci", version = "2.0.1") bazel_dep(name = "rules_pkg", version = "1.0.1") bazel_dep(name = "rules_shell", version = "0.4.1") # -- bazel_dep definitions -- # non_module_deps = use_extension("//:non_module_deps.bzl", "non_module_deps") use_repo(non_module_deps, "kubernetes_helm") use_repo(non_module_deps, "kubernetes_helm3") use_repo(non_module_deps, "hashicorp_terraform") use_repo(non_module_deps, "com_github_kubernetes_sigs_application") use_repo(non_module_deps, "ingress-nginx") # End of extension `non_module_deps` ####### # C++ # ####### bazel_dep(name = "toolchains_llvm", version = "1.7.0") # Inspect supported toolchains at https://github.com/bazel-contrib/toolchains_llvm/blob/master/toolchain/internal/llvm_distributions.bzl llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm") llvm.toolchain( llvm_version = "18.1.4", ) use_repo(non_module_deps, "com_googleapis_storage_chrome_linux_amd64_sysroot") llvm.sysroot( label = "@com_googleapis_storage_chrome_linux_amd64_sysroot//:all_files", targets = ["linux-x86_64"], ) use_repo(llvm, "llvm_toolchain") register_toolchains("@llvm_toolchain//:all") bazel_dep(name = "rules_cc", version = "0.1.5") ###### # Go # ###### bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go") go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk") go_sdk.download(version = "1.25.4") bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle") go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//src:go.mod") use_repo( go_deps, "com_github_cenkalti_backoff", "com_github_form3tech_oss_jwt_go", "com_github_fsnotify_fsnotify", "com_github_getlantern_httptest", "com_github_golang_glog", "com_github_golang_mock", "com_github_google_go_cmp", "com_github_google_nftables", "com_github_googlecloudrobotics_ilog", "com_github_jaypipes_ghw", "com_github_jaypipes_pcidb", "com_github_motemen_go_loghttp", "com_github_onsi_gomega", "com_github_pkg_errors", "com_github_prometheus_client_golang", "com_github_spf13_cobra", "com_github_spf13_pflag", "com_google_cloud_go_compute_metadata", "com_google_cloud_go_storage", "in_gopkg_h2non_gock_v1", "io_k8s_api", "io_k8s_apiextensions_apiserver", "io_k8s_apimachinery", "io_k8s_cli_runtime", "io_k8s_client_go", "io_k8s_helm", "io_k8s_klog", "io_k8s_klog_v2", "io_k8s_sigs_controller_runtime", "io_k8s_sigs_kind", "io_k8s_sigs_yaml", "io_opencensus_go", "io_opencensus_go_contrib_exporter_prometheus", "io_opencensus_go_contrib_exporter_stackdriver", "org_golang_google_api", "org_golang_google_grpc", "org_golang_google_protobuf", "org_golang_x_crypto", "org_golang_x_net", "org_golang_x_oauth2", "org_golang_x_sync", ) ####### # OCI # ####### oci = use_extension("@rules_oci//oci:extensions.bzl", "oci") # gcloud container images describe gcr.io/distroless/base:latest --format='value(image_summary.digest)' oci.pull( name = "distroless_base", digest = "sha256:b31a6e02605827e77b7ebb82a0ac9669ec51091edd62c2c076175e05556f4ab9", image = "gcr.io/distroless/base", platforms = ["linux/amd64"], ) # gcloud container images describe gcr.io/distroless/cc:latest --format='value(image_summary.digest)' oci.pull( name = "distroless_cc", digest = "sha256:8aad707f96620ee89e27febef51b01c6ff244277a3560fcfcfbe68633ef09193", image = "gcr.io/distroless/cc", platforms = ["linux/amd64"], ) oci.pull( name = "iptables_base", digest = "sha256:656e45c00083359107b1d6ae0411ff3894ba23011a8533e229937a71be84e063", image = "gcr.io/google-containers/debian-iptables", platforms = ["linux/amd64"], ) use_repo( oci, "distroless_base", "distroless_base_linux_amd64", "distroless_cc", "distroless_cc_linux_amd64", "iptables_base", "iptables_base_linux_amd64", ) ================================================ FILE: README.md ================================================ # Cloud Robotics Core Google's Cloud Robotics Core is an open source platform that provides infrastructure essential to building and running robotics solutions for business automation. Cloud Robotics Core makes managing robot fleets easy for developers, integrators, and operators. It enables: * packaging and distribution of applications * secure, bidirectional robot-cloud communication * easy access to Google Cloud services such as ML, logging, and monitoring. ![Cloud Robotics Core overview](docs/cloud-robotics-core-overview.png) Cloud Robotics Core is open source and pre-alpha. Support is currently limited to a small set of early access partners. We will gladly accept contributions and feedback, but we are making no stability or support guarantees at this point in time. # Documentation Documentation of the platform and related How-to guides can be found at: https://googlecloudrobotics.github.io/core/ # Get Involved If you want to get involved, please refer to [CONTRIBUTING.md](CONTRIBUTING.md), reach out to [cloud-robotics-discuss@googlegroups.com](https://groups.google.com/forum/#!forum/cloud-robotics-discuss) or ask Stack Overflow questions with [#google-cloud-robotics](https://stackoverflow.com/questions/tagged/google-cloud-robotics). # Source Code Most interesting bits are under `src`: * app_charts: contains kubernetes resources for the core platform and apps * bootstrap: provisioning for the cloud (terraform) and the robot (debian package) * go/: the code that goes into images referenced from `app_charts` The root directory contains a `deploy.sh` script for building and installing the software. More details on that are in the [building from sources](how-to/deploy-from-sources) guide. ================================================ FILE: bazel/BUILD.bazel ================================================ exports_files([ "app.bzl", "app_chart.bzl", "container_push.bzl", "repositories.bzl", ]) platform( name = "linux_x86_64", constraint_values = [ "@platforms//os:linux", "@platforms//cpu:x86_64", "@bazel_tools//tools/cpp:clang", ], exec_properties = { "container-image": "docker://gcr.io/cloud-robotics-releases/bazel-rbe-executor@sha256:3ee043e7a322caaff8c9edaa302373deb80e67ad6e42ae35d34b8f3597b8995e", "OSFamily": "Linux", }, parents = ["@local_config_platform//:host"], ) ================================================ FILE: bazel/BUILD.sysroot ================================================ filegroup( name = "all_files", srcs = glob( [ "lib/x86_64-linux-gnu/ld*", "lib/x86_64-linux-gnu/libc*", "lib/x86_64-linux-gnu/libdl*", "lib/x86_64-linux-gnu/libgcc*", "lib/x86_64-linux-gnu/libm*", "lib/x86_64-linux-gnu/libpthread*", "lib/x86_64-linux-gnu/librt*", "lib/x86_64-linux-gnu/libutil*", "lib64/**", "usr/include/*.h", "usr/include/arpa/**", "usr/include/asm-generic/**", "usr/include/c++/**", "usr/include/linux/**", "usr/include/net/**", "usr/include/netinet/**", "usr/include/rpc/**", "usr/include/sys/**", "usr/include/x86_64-linux-gnu/**", "usr/lib/gcc/**", "usr/lib/x86_64-linux-gnu/*crt*.o", "usr/lib/x86_64-linux-gnu/libc_nonshared.a", "usr/lib/x86_64-linux-gnu/libc.a", "usr/lib/x86_64-linux-gnu/libc.so", "usr/lib/x86_64-linux-gnu/libdl*", "usr/lib/x86_64-linux-gnu/libm*", "usr/lib/x86_64-linux-gnu/libpthread*", "usr/lib/x86_64-linux-gnu/libresolv.so", "usr/lib/x86_64-linux-gnu/librt*", "usr/lib/x86_64-linux-gnu/libutil*", ], ), visibility = ["//visibility:public"], ) ================================================ FILE: bazel/app.bzl ================================================ load("//bazel/build_rules/app_chart:run_parallel.bzl", "run_parallel") def app(name, charts, visibility = None): """Macro for a standard Cloud Robotics app. This macro establishes two subrules for app name "foo": - :foo.push pushes the Docker images for the app. - :foo.yaml is a YAML file with the app CR that you need to push to Kubernetes. Use k8s_object to push it, or compile it into a Helm chart. Args: name: string. Name of the app. charts: list of targets. Helm charts for this app. visibility: Visibility. """ pkg = Label("{}//{}".format(native.repository_name(), native.package_name())) chart_labels = [pkg.relative(c) for c in charts] run_parallel( name = name + ".push", targets = ["//{}:{}.push".format(c.package, c.name) for c in chart_labels], visibility = visibility, ) native.genrule( # we name this differently than the file we produce to silence: # target 'xxx.yaml' is both a rule and a file; please choose another name for the rule name = name + ".manifest", srcs = [ "//{}:{}.snippet-yaml".format(c.package, c.name) for c in chart_labels ], outs = [name + ".yaml"], cmd = """cat - $(SRCS) > $@ < {} < $@ {target}: inline: $$(base64 -w 0 $<) EOF """.format(name = name, target = chart), ) ================================================ FILE: bazel/build_rules/app_chart/BUILD.bazel ================================================ exports_files([ "cache_gcr_credentials.sh.tpl", "Chart.yaml.template", "push_all.sh.tpl", "run_parallel.sh.tpl", "values-cloud.yaml", "values-robot.yaml", ]) ================================================ FILE: bazel/build_rules/app_chart/Chart.yaml.template ================================================ apiVersion: v1 name: ${name} version: ${version} # Linter expects an icon. icon: https://google.com/icon.png ================================================ FILE: bazel/build_rules/app_chart/cache_gcr_credentials.bzl ================================================ def _get_runfile_path(ctx, f): """Return the runfiles relative path of f.""" if ctx.workspace_name: return "${RUNFILES}/" + ctx.workspace_name + "/" + f.short_path else: return "${RUNFILES}/" + f.short_path def _impl(ctx): runfiles = ctx.attr._sh_tpl.default_runfiles.files.to_list() runfiles.append(ctx.attr.target.files_to_run.executable) runfiles.extend(ctx.attr.target.default_runfiles.files.to_list()) variables = "PYTHON_RUNFILES=\"${RUNFILES}\" " ctx.actions.expand_template( template = ctx.file._sh_tpl, substitutions = { "%{gcr_registry}": ctx.attr.gcr_registry, "%{command}": variables + _get_runfile_path(ctx, ctx.attr.target.files_to_run.executable), }, output = ctx.outputs.executable, is_executable = True, ) return [DefaultInfo(runfiles = ctx.runfiles(files = runfiles))] cache_gcr_credentials = rule( attrs = { "target": attr.label( mandatory = True, ), "gcr_registry": attr.string( default = "gcr.io", doc = "If set, credentials for this GCR registry's domain will be precached", ), "_sh_tpl": attr.label( default = Label("//bazel/build_rules/app_chart:cache_gcr_credentials.sh.tpl"), allow_single_file = True, ), }, executable = True, implementation = _impl, ) """Cache gcr credentials before running a command. This rule executes docker-credential-gcloud before running the command, and replaces the binary with a helper that is safe for concurrent execution. Works around around https://github.com/bazelbuild/rules_docker/issues/511. Args: target: A target that can be run with "bazel run". gcr_registry: string. A GCR Docker registry (gcr.io/myproject). """ ================================================ FILE: bazel/build_rules/app_chart/cache_gcr_credentials.sh.tpl ================================================ #!/usr/bin/env bash set -eu function guess_runfiles() { pushd ${BASH_SOURCE[0]}.runfiles > /dev/null 2>&1 pwd popd > /dev/null 2>&1 } RUNFILES="${PYTHON_RUNFILES:-$(guess_runfiles)}" # app() uses run_parallel() to push images to GCR, which relies on # gcloud to get credentials. That, however, has a race condition: # https://github.com/google/containerregistry/issues/115 # As such, we cache credentials and create a script that prints them to # replace the racy credential helper. This script is added to to the start of # PATH. This is harmless if run_parallel() is being used for something else. # It also saves ~5s of CPU time. tmp_bin=$(mktemp --tmpdir= -d deploy-XXXXXXXX-bin) export PATH="${tmp_bin}:${PATH}" function rm_tmp_bin { rm -r "${tmp_bin}" } trap rm_tmp_bin EXIT credential_script=$tmp_bin/docker-credential-gcloud credential_file=$tmp_bin/docker-credential-gcloud.json gcp_registry=$(echo "%{gcr_registry}" | cut -d'/' -f 1) docker-credential-gcloud get <<<"https://${gcp_registry}" > "${credential_file}" cat > "${credential_script}" << EOF #!/bin/bash cat "${credential_file}" EOF chmod +x "${credential_script}" %{command} "$@" ================================================ FILE: bazel/build_rules/app_chart/push_all.bzl ================================================ load("//bazel:container_push.bzl", "container_push") def _get_runfile_path(ctx, f): """Return the runfiles relative path of f.""" if ctx.workspace_name: return "${RUNFILES}/" + ctx.workspace_name + "/" + f.short_path else: return "${RUNFILES}/" + f.short_path def _impl(ctx): runfiles = ctx.attr._sh_tpl.default_runfiles.files.to_list() for target in ctx.attr.push_targets: runfiles.append(target.files_to_run.executable) runfiles.extend(target.default_runfiles.files.to_list()) ctx.actions.expand_template( template = ctx.file._sh_tpl, substitutions = { "%{commands}": "\n".join( [ "if [[ -z \"${TAG:-}\" ]]; then echo >&2 \"$0: TAG environment variable must be set when pushing images.\"; exit 1; fi", ] + [ "async {command} --repository=\"${{CONTAINER_REGISTRY}}/{repository}\" --tag=\"${{TAG}}\"".format( command = _get_runfile_path(ctx, target.files_to_run.executable), repository = repository, ) for target, repository in zip(ctx.attr.push_targets, ctx.attr.images.keys()) ], ), }, output = ctx.outputs.executable, is_executable = True, ) return [DefaultInfo(runfiles = ctx.runfiles(files = runfiles))] _push_all = rule( attrs = { # Implicit dependencies. "push_targets": attr.label_list( allow_files = True, ), "images": attr.string_dict( default = {}, ), "_sh_tpl": attr.label( default = Label("//bazel/build_rules/app_chart:push_all.sh.tpl"), allow_single_file = True, ), }, executable = True, implementation = _impl, ) def push_all(name, images = {}, **kwargs): """Creates a script to push several container images to a docker registry. The registry has to be specified as parameter when invoking the script. Args: images: dict. Repository names as keys and images to be pushed as values. """ if "push_targets" in kwargs: fail("reserved for internal use by push_all macro", attr = "push_targets") images = images or {} push_targets = [] for repository, image in images.items(): push_target = name + "." + repository + ".push" push_targets.append(push_target) container_push( name = push_target, image = image, ) _push_all(name = name, images = images, push_targets = push_targets, **kwargs) ================================================ FILE: bazel/build_rules/app_chart/push_all.sh.tpl ================================================ #!/usr/bin/env bash set -eu if [[ "$#" -lt 1 ]]; then echo "Usage: $0 " exit 1 fi CONTAINER_REGISTRY="$1" function guess_runfiles() { pushd ${BASH_SOURCE[0]}.runfiles > /dev/null 2>&1 pwd popd > /dev/null 2>&1 } RUNFILES="${PYTHON_RUNFILES:-$(guess_runfiles)}" PIDS=() function async() { # Launch the command asynchronously and track its process id. PYTHON_RUNFILES=${RUNFILES} "$@" & PIDS+=($!) } %{commands} if [[ "${#PIDS[@]}" = 0 ]]; then # It is valid to generate this script without pushing any images. # Bash before v4.4 considers an empty array an unbound variable and would # choke on the for-loop below. exit 0 fi # Wait for all of the subprocesses, failing the script if any of them failed. exitcode=0 for pid in "${PIDS[@]}"; do wait ${pid} || exitcode=$? done exit $exitcode ================================================ FILE: bazel/build_rules/app_chart/run_parallel.bzl ================================================ def _get_runfile_path(ctx, f): """Return the runfiles relative path of f.""" if ctx.workspace_name: return "${RUNFILES}/" + ctx.workspace_name + "/" + f.short_path else: return "${RUNFILES}/" + f.short_path def _impl(ctx): runfiles = ctx.attr._sh_tpl.default_runfiles.files.to_list() for target in ctx.attr.targets: runfiles.append(target.files_to_run.executable) runfiles.extend(target.default_runfiles.files.to_list()) ctx.actions.expand_template( template = ctx.file._sh_tpl, substitutions = { "%{commands}": "\n".join([ "async \"%s\" \"$@\"" % _get_runfile_path(ctx, command.files_to_run.executable) for command in ctx.attr.targets ]), }, output = ctx.outputs.executable, is_executable = True, ) return [DefaultInfo(runfiles = ctx.runfiles(files = runfiles))] run_parallel = rule( attrs = { "targets": attr.label_list( allow_empty = False, mandatory = True, ), "_sh_tpl": attr.label( default = Label("//bazel/build_rules/app_chart:run_parallel.sh.tpl"), allow_single_file = True, ), }, executable = True, implementation = _impl, ) """Run multiple targets in parallel. This rule builds a "bazel run" target that runs a series of subtargets in parallel. If a subtarget has errors, execution results in an error when all subtargets have completed. Args: targets: A list of targets that can be run with "bazel run". """ ================================================ FILE: bazel/build_rules/app_chart/run_parallel.sh.tpl ================================================ #!/usr/bin/env bash set -eu function guess_runfiles() { pushd ${BASH_SOURCE[0]}.runfiles > /dev/null 2>&1 pwd popd > /dev/null 2>&1 } RUNFILES="${PYTHON_RUNFILES:-$(guess_runfiles)}" PIDS=() function async() { # Launch the command asynchronously and track its process id. PYTHON_RUNFILES=${RUNFILES} "$@" & PIDS+=($!) } %{commands} # Wait for all of the subprocesses, failing the script if any of them failed. exitcode=0 for pid in "${PIDS[@]}"; do wait ${pid} || exitcode=$? done exit $exitcode ================================================ FILE: bazel/build_rules/app_chart/values-cloud.yaml ================================================ domain: "example.com" project: "my-gcp-project" deploy_environment: "GCP" registry: "gcr.io/my-gcp-project" robots: [] region: example-gcp-region # Token Vendor feature flags use_tv_k8s_verbose: false ================================================ FILE: bazel/build_rules/app_chart/values-robot.yaml ================================================ domain: "example.com" project: "my-gcp-project" deploy_environment: "GCP" registry: "gcr.io/my-gcp-project" robot: name: "" ================================================ FILE: bazel/build_rules/copy.bzl ================================================ """Macros to help rearrange files.""" def copy_files(name, srcs, outdir, visibility = None): """Creates copies of files within a directory. For each source file, a copy with the same basename is created in outdir. A filegroup target is created containing the new files. Args: srcs: list of files. outdir: output directory. visibility: handled through to the targets. """ outs = [] for src in srcs: filename = src.split("/")[-1] out = "%s/%s" % (outdir, filename) native.genrule( name = "%s_%s" % (name, filename), srcs = [src], outs = [out], cmd = "cp $< $@", visibility = visibility, ) outs.append(out) native.filegroup( name = name, srcs = outs, visibility = visibility, ) ================================================ FILE: bazel/build_rules/helm_chart.bzl ================================================ def helm_chart(ctx, name, chart, files, templates, values, version, helm, out): """Starlark function that builds a helm chart. Args: name: string. Must match the name in Chart.yaml. chart: file. The Chart.yaml file. files: list of non-template files to put in files/. templates: list of template files. values: file. The values.yaml file. version: string. Overwrites any version in Chart.yaml. helm: file. The Helm tool. out: file. The file that the chart is built to. """ cmd = """ mkdir {name} {name}/templates cp {chart} {name}/Chart.yaml cp {values} {name}/values.yaml """.format(name = name, chart = chart.path, values = values.path) if templates: template_files = " ".join([t.path for t in templates]) # Use a single cp invocation to detect filename clashes. cmd += "cp {templates} {name}/templates\n".format(name = name, templates = template_files) if files: cmd += "mkdir {name}/files\n".format(name = name) files_locations = " ".join([f.path for f in files]) # Use a single cp invocation to detect filename clashes. cmd += "cp {files} {name}/files\n".format(name = name, files = files_locations) cmd += """ # Linter is too noisy, swallow its output when not failing {helm} lint --strict {name} >/dev/null 2>&1 || \\ {helm} lint --strict {name} {helm} package \\ --save=false --version={version} {name} \\ | (grep -v "Successfully packaged" || true) mv $(basename {output}) {output} rm -rf {name}""".format(name = name, version = version, helm = helm.path, output = out.path) ctx.actions.run_shell( inputs = [chart, values] + templates + (files or []), tools = [helm], outputs = [out], command = cmd, toolchain = None, ) ================================================ FILE: bazel/build_rules/helm_template.bzl ================================================ def helm_template(name, release_name, chart, values, namespace = None, helm_version = 2): """Locally expand a helm chart. Args: chart: build label, referencing the chart to expand. values: label. File with expand-time values. """ tool = "" cmd = "" if helm_version == 2: tool = "@kubernetes_helm//:helm" cmd = "$(location {tool}) template --name {name} --namespace {namespace} --values $(location {values}) $(location {chart}) > $@".format(name = release_name, namespace = namespace or "default", chart = chart, tool = tool, values = values) elif helm_version == 3: tool = "@kubernetes_helm3//:helm" cmd = "$(location {tool}) template {name} $(location {chart}) --namespace {namespace} --values $(location {values}) > $@".format(name = release_name, namespace = namespace or "default", chart = chart, tool = tool, values = values) else: fail("Unsupported helm version. Expected {2,3}, got ", helm_version) native.genrule( name = name, srcs = [chart, values], outs = [name + ".yaml"], cmd = cmd, tools = [tool], ) ================================================ FILE: bazel/container_push.bzl ================================================ load("@rules_oci//oci:defs.bzl", "oci_push") def container_push(*args, **kwargs): """Creates a script to push a container image to a Docker registry. The target name must be specified when invoking the push script.""" if "repository" in kwargs: fail( "Cannot set 'repository' attribute on container_push", attr = "repository", ) kwargs["repository"] = "IGNORE" oci_push(*args, **kwargs) ================================================ FILE: bazel/debug_repository.bzl ================================================ """Debug util for repository definitions.""" def debug_repository(repo, *fields): """debug_repository(repo) identifies which version of a repository has been defined in the WORKSPACE by printing some of its fields. Example: # at the bottom of the WORKSPACE file load("//bazel:debug_repository.bzl", "debug_repository") debug_repository("org_golang_x_net") If needed, you can override the printed fields by passing additional parameters: debug_repository("io_grpc_grpc_java", "patches", "urls") """ if len(fields) == 0: fields = ["branch", "commit", "tag", "url", "urls"] rule = native.existing_rule(repo) if rule == None: print(repo, "not found") return for f in fields: if f in rule and len(rule[f]) > 0: print(repo, f, rule[f]) ================================================ FILE: config.sh.tmpl ================================================ #!/usr/bin/env bash ### Required settings ### # Project ID of your Cloud Robotics GCP project. This project can be created # for you as part of the Terraform setup, or it can be created and configured # manually, then imported with `deploy.sh set-project` or `terraform import`. GCP_PROJECT_ID=my-project # GCP region and zone where resources should be created. GCP_REGION=europe-west1 GCP_ZONE=europe-west1-c ### Optional settings ### # The Docker registry all Cloud Robotics images are deployed to when installing # from sources. It is ignored during binary installs. # If unset, defaults to "gcr.io/${GCP_PROJECT_ID}" #CLOUD_ROBOTICS_CONTAINER_REGISTRY=gcr.io/my-project # A space-separated list of GCP alphanumeric project IDs for private image # repositories. The installer will provision GCR access to these projects, # both for the gke-node service account and for the robot service account. #PRIVATE_DOCKER_PROJECTS="my-project my-other-project" # A Google Group that should be a co-owner of the created GCP project. #CLOUD_ROBOTICS_SHARED_OWNER_GROUP=my-group@googlegroups.com # If you want to store your Terraform state in a GCS bucket, give a bucket name # and a subdirectory of the bucket here. See # https://www.terraform.io/docs/backends/types/gcs.html for docs. #TERRAFORM_GCS_BUCKET="my-gcs-bucket" #TERRAFORM_GCS_PREFIX="my/sub/directory" # Symmetric cookie encryption key for the oauth2-proxy. Generate with: # python -c 'import os,base64; print base64.urlsafe_b64encode(os.urandom(16))' #CLOUD_ROBOTICS_COOKIE_SECRET=A_CyACoujODhfn2yDMy5tw== # Oauth2 client ID and client secret from # https://console.cloud.google.com/apis/credentials. If you leave these empty, # you won't be able to log in with a browser (but CLI access will work fine). #CLOUD_ROBOTICS_OAUTH2_CLIENT_ID=....apps.googleusercontent.com #CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET=... # Domain to be used for the ingress # If unset, defaults to "www.endpoints.${GCP_PROJECT_ID}.cloud.goog" #CLOUD_ROBOTICS_DOMAIN=www.example.com # Enable google cloud robotics layer 2 APP_MANAGEMENT=true # Enable google cloud robotics layer 1 ONPREM_FEDERATION=true # Disable the secret manager integration by default GKE_SECRET_MANAGER_PLUGIN=false ================================================ FILE: current_versions.txt ================================================ { "cert-manager": "1.16.3", "ingress-nginx": "1.8.4", "oauth2-proxy": "7.5.1", "stackdriver-logging-agent": "1.9.5" } ================================================ FILE: deploy.sh ================================================ #!/bin/bash # # Copyright 2019 The Cloud Robotics Authors # # 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. # Manage a deployment DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/scripts/common.sh" source "${DIR}/scripts/config.sh" source "${DIR}/scripts/include-config.sh" set -o pipefail -o errexit PROJECT_NAME="cloud-robotics" RUNFILES_ROOT="_main" if is_source_install; then # Not using bazel run to not clobber the bazel-bin dir TERRAFORM="${DIR}/bazel-out/../../../external/+non_module_deps+hashicorp_terraform/terraform" HELM_COMMAND="${DIR}/bazel-out/../../../external/+non_module_deps+kubernetes_helm/helm" SYNK_COMMAND="${DIR}/bazel-bin/src/go/cmd/synk/synk_/synk" else TERRAFORM="${DIR}/bin/terraform" HELM_COMMAND="${DIR}/bin/helm" SYNK_COMMAND="${DIR}/bin/synk" fi TERRAFORM_DIR="${DIR}/src/bootstrap/cloud/terraform" TERRAFORM_APPLY_FLAGS=${TERRAFORM_APPLY_FLAGS:- -auto-approve} # utility functions function include_config_and_defaults { include_config "$1" CLOUD_ROBOTICS_DOMAIN=${CLOUD_ROBOTICS_DOMAIN:-"www.endpoints.${GCP_PROJECT_ID}.cloud.goog"} APP_MANAGEMENT=${APP_MANAGEMENT:-false} ONPREM_FEDERATION=${ONPREM_FEDERATION:-true} GKE_SECRET_MANAGER_PLUGIN=${GKE_SECRET_MANAGER_PLUGIN:-false} # lets-encrypt is used as the default certificate provider for backwards compatibility purposes CLOUD_ROBOTICS_CERTIFICATE_PROVIDER=${CLOUD_ROBOTICS_CERTIFICATE_PROVIDER:-lets-encrypt} CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME=${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME:-GCP_PROJECT_ID} CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION=${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION:-GCP_PROJECT_ID} CLOUD_ROBOTICS_OWNER_EMAIL=${CLOUD_ROBOTICS_OWNER_EMAIL:-$(gcloud config get-value account)} CLOUD_ROBOTICS_CTX=${CLOUD_ROBOTICS_CTX:-"gke_${GCP_PROJECT_ID}_${GCP_ZONE}_${PROJECT_NAME}"} } function update_config_var { cloud_bucket="gs://${1}-cloud-robotics-config" name="${2}" value="${3}" config_file="$(mktemp)" gcloud storage cp "${cloud_bucket}/config.sh" "${config_file}" 2>/dev/null || return save_variable "${config_file}" "${name}" "${value}" gcloud storage mv "${config_file}" "${cloud_bucket}/config.sh" } function prepare_source_install { # For whatever reasons different combinations of bazel environemnt seem to # work differently wrt bazel-bin. This hack ensure that both synk and the # files that synk will install are in bazel-bin tmpdir="$(mktemp -d)" bazel ${BAZEL_FLAGS} build //src/go/cmd/synk cp -a ${DIR}/bazel-bin/src/go/cmd/synk/synk_/synk ${tmpdir}/synk bazel ${BAZEL_FLAGS} build \ "@hashicorp_terraform//:terraform" \ "@kubernetes_helm//:helm" \ //src/app_charts/base:base-cloud \ //src/app_charts/platform-apps:platform-apps-cloud \ //src/app_charts:push \ //src/bootstrap/cloud:setup-robot.digest \ //src/go/cmd/setup-robot:setup-robot.push mkdir -p ${DIR}/bazel-bin/src/go/cmd/synk/synk_/ mv -n ${tmpdir}/synk ${DIR}/bazel-bin/src/go/cmd/synk/synk_/synk rm -f ${tmpdir}/synk rmdir ${tmpdir} || /bin/true # TODO(rodrigoq): the artifactregistry API would be enabled by Terraform, but # that doesn't run until later, as it needs the digest of the setup-robot # image. Consider splitting prepare_source_install into source_install_build # and source_install_push and using Terraform to enable the API in between. gcloud services enable artifactregistry.googleapis.com \ --project "${GCP_PROJECT_ID}" # `setup-robot.push` is the first container push to avoid a GCR bug with parallel pushes on newly # created projects (see b/123625511). local oldPwd oldPwd=$(pwd) cd ${DIR}/bazel-bin/src/go/cmd/setup-robot/push_setup-robot.push.sh.runfiles/${RUNFILES_ROOT} ${DIR}/bazel-bin/src/go/cmd/setup-robot/push_setup-robot.push.sh \ --repository="${CLOUD_ROBOTICS_CONTAINER_REGISTRY}/setup-robot" \ --tag="latest" # The tag variable must be called 'TAG', see cloud-robotics/bazel/container_push.bzl # Running :push outside the build system shaves ~3 seconds off an incremental build. cd ${DIR}/bazel-bin/src/app_charts/push.runfiles/${RUNFILES_ROOT} TAG="latest" ${DIR}/bazel-bin/src/app_charts/push "${CLOUD_ROBOTICS_CONTAINER_REGISTRY}" cd ${oldPwd} } function terraform_exec { ( cd "${TERRAFORM_DIR}" && ${TERRAFORM} "$@" ) } function terraform_init { local ROBOT_IMAGE_DIGEST ROBOT_IMAGE_DIGEST=$(cat bazel-bin/src/bootstrap/cloud/setup-robot.digest) # We only need to create dns resources if a custom domain is used. local CUSTOM_DOMAIN if [[ "${CLOUD_ROBOTICS_DOMAIN}" != "www.endpoints.${GCP_PROJECT_ID}.cloud.goog" ]]; then CUSTOM_DOMAIN="${CLOUD_ROBOTICS_DOMAIN}" fi # This variable is set by src/bootstrap/cloud/run-install.sh for binary installs local CRC_VERSION if [[ -z "${TARGET}" ]]; then # TODO(ensonic): keep this in sync with the nightly release script VERSION=${VERSION:-"0.1.0"} if [[ -d .git ]]; then SHA=$(git rev-parse --short HEAD) else echo "WARNING: no git dir and no \$TARGET env set" SHA="unknown" fi CRC_VERSION="crc-${VERSION}/crc-${VERSION}+${SHA}" else CRC_VERSION="${TARGET%.tar.gz}" fi cat > "${TERRAFORM_DIR}/terraform.tfvars" <> "${TERRAFORM_DIR}/terraform.tfvars" <> "${TERRAFORM_DIR}/terraform.tfvars" <> "${TERRAFORM_DIR}/terraform.tfvars" local AR for AR in "${ADDITIONAL_REGIONS[@]}"; do local AR_NAME local AR_REGION local AR_ZONE AR_NAME=$(jq -r .name <<<"${AR}") AR_REGION=$(jq -r .region <<<"${AR}") AR_ZONE=$(jq -r .zone <<<"${AR}") cat >> "${TERRAFORM_DIR}/terraform.tfvars" <> "${TERRAFORM_DIR}/terraform.tfvars" # Docker private projects if [[ -n "${PRIVATE_DOCKER_PROJECTS:-}" ]]; then cat >> "${TERRAFORM_DIR}/terraform.tfvars" < "${TERRAFORM_DIR}/backend.tf" </dev/null; do sleep 1 i=$((i + 1)) if ((i >= 60)) ; then # Try again, without suppressing stderr this time. if ! kubectl --context "${CLUSTER_CONTEXT}" get serviceaccount default >/dev/null; then die "'kubectl get serviceaccount default' failed" fi fi done local BASE_NAMESPACE BASE_NAMESPACE="default" # Remove old unmanaged cert if ! kubectl --context "${CLUSTER_CONTEXT}" get secrets cluster-authority -o yaml | grep -q "cert-manager.io/certificate-name: selfsigned-ca"; then kubectl --context "${CLUSTER_CONTEXT}" delete secrets cluster-authority 2> /dev/null || true fi # Delete permissive binding if it exists from previous deployments if kubectl --context "${CLUSTER_CONTEXT}" get clusterrolebinding permissive-binding &>/dev/null; then kubectl --context "${CLUSTER_CONTEXT}" delete clusterrolebinding permissive-binding fi local values values=( --set-string "domain=${CLUSTER_DOMAIN}" --set-string "ingress_ip=${INGRESS_IP}" --set-string "project=${GCP_PROJECT_ID}" --set-string "region=${CLUSTER_REGION}" --set-string "registry=${SOURCE_CONTAINER_REGISTRY}" --set-string "owner_email=${CLOUD_ROBOTICS_OWNER_EMAIL}" --set-string "app_management=${APP_MANAGEMENT}" --set-string "onprem_federation=${ONPREM_FEDERATION}" --set-string "certificate_provider=${CLOUD_ROBOTICS_CERTIFICATE_PROVIDER}" --set-string "deploy_environment=${CLOUD_ROBOTICS_DEPLOY_ENVIRONMENT}" --set-string "oauth2_proxy.client_id=${CLOUD_ROBOTICS_OAUTH2_CLIENT_ID}" --set-string "oauth2_proxy.client_secret=${CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET}" --set-string "oauth2_proxy.cookie_secret=${CLOUD_ROBOTICS_COOKIE_SECRET}" --set "use_tv_verbose=${CRC_USE_TV_VERBOSE}" ) ${SYNK_COMMAND} --context "${CLUSTER_CONTEXT}" init echo "synk init done" echo "installing base-cloud to ${CLUSTER_CONTEXT}..." ${HELM_COMMAND} --kube-context "${CLUSTER_CONTEXT}" template -n base-cloud --namespace=${BASE_NAMESPACE} "${values[@]}" \ ./bazel-bin/src/app_charts/base/base-cloud-0.0.1.tgz \ | ${SYNK_COMMAND} --context "${CLUSTER_CONTEXT}" apply base-cloud -n ${BASE_NAMESPACE} -f - \ || die "Synk failed for base-cloud" # This is the main region. Only run this here! if [[ "${CLUSTER_NAME}" = "${PROJECT_NAME}" ]]; then echo "installing platform-apps-cloud to ${CLOUD_ROBOTICS_CTX}..." ${HELM_COMMAND} --kube-context "${CLUSTER_CONTEXT}" template -n platform-apps-cloud "${values[@]}" \ ./bazel-bin/src/app_charts/platform-apps/platform-apps-cloud-0.0.1.tgz \ | ${SYNK_COMMAND} --context "${CLUSTER_CONTEXT}" apply platform-apps-cloud -f - \ || die "Synk failed for platform-apps-cloud" fi } function helm_main_region { local INGRESS_IP INGRESS_IP=$(terraform_exec output ingress-ip | tr -d '"') helm_region_shared \ "${CLOUD_ROBOTICS_CTX}" \ "${CLOUD_ROBOTICS_DOMAIN}" \ "${INGRESS_IP}" \ "${GCP_REGION}" \ "${GCP_ZONE}" \ "${PROJECT_NAME}" } function helm_additional_region { local ar_description ar_description="${1}" local AR_NAME local AR_REGION local AR_ZONE AR_NAME=$(jq -r .name <<<"${ar_description}") AR_REGION=$(jq -r .region <<<"${ar_description}") AR_ZONE=$(jq -r .zone <<<"${ar_description}") local CLUSTER_NAME CLUSTER_NAME="${AR_NAME}-ar-cloud-robotics" local INGRESS_IP INGRESS_IP=$(terraform_exec output -json ingress-ip-ar | jq -r ."\"${CLUSTER_NAME}\"") helm_region_shared \ $(gke_context_name "${GCP_PROJECT_ID}" "${CLUSTER_NAME}" "${AR_REGION}" "${AR_ZONE}") \ "${AR_NAME}.${CLOUD_ROBOTICS_DOMAIN}" \ "${INGRESS_IP}" \ "${AR_REGION}" \ "${AR_ZONE}" \ "${CLUSTER_NAME}" } function helm_charts { helm_main_region local AR for AR in "${ADDITIONAL_REGIONS[@]}"; do helm_additional_region "${AR}" done } # commands function set_config { local project_id="$1" ${DIR}/scripts/set-config.sh "${project_id}" } function create { include_config_and_defaults $1 if is_source_install; then prepare_source_install fi terraform_apply helm_charts } function delete { include_config_and_defaults $1 if is_source_install; then bazel ${BAZEL_FLAGS} build "@hashicorp_terraform//:terraform" fi terraform_delete } # Alias for create. function update { create $1 } # This is a shortcut for skipping Terraform config checks if you know the config has not changed. function fast_push { include_config_and_defaults $1 if is_source_install; then prepare_source_install fi helm_charts } # This is a shortcut for skipping building and applying Terraform configs if you know the build has not changed. function update_infra { include_config_and_defaults $1 terraform_apply } # main if [[ "$#" -lt 2 ]] || [[ ! "$1" =~ ^(set_config|create|delete|update|fast_push|update_infra)$ ]]; then die "Usage: $0 {set_config|create|delete|update|fast_push|update_infra} " fi # log and call arguments verbatim: log $2 $0 $1 "$@" ================================================ FILE: docs/.gitignore ================================================ # Files created when running Jekyll locally, following # https://help.github.com/en/articles/setting-up-your-github-pages-site-locally-with-jekyll .sass-cache/ _site/ Gemfile Gemfile.lock ================================================ FILE: docs/_config.yml ================================================ theme: jekyll-theme-slate ================================================ FILE: docs/concepts/app-management.md ================================================ # App Management The Cloud Robotics Core application management (Layer 2) makes it easy to define and deploy arbitrary applications across a fleet of cloud and robot clusters. The [Helm v2 chart format](https://helm.sh/docs/developing_charts/) is used to define an application at the scope of a single cluster. Additional custom resources tie them together to a cross-cluster application and define their deployment. It relies on the Cloud Robotics Core [federation layer](federation.md) to distribute the resources to the right clusters. ## App resource The App resource defines a Cloud Robotics Core application by simply describing Helm charts for two classes of clusters: cloud and robots. Charts may be specified inline as base64-encoded Helm chart files or by referencing a Helm repository. App resources will be deployed to the cloud cluster. Example 1: application referencing charts from helm repositories ```yaml apiVersion: apps.cloudrobotics.com/v1alpha1 kind: App metadata: name: ros-v1 spec: repository: https://my.repo version: 1.2.1 components: cloud: name: ros-cloud robot: name: ros-robot ``` Example 2: application using inline as base64-encoded charts (development workflow) ```yaml apiVersion: apps.cloudrobotics.com/v1alpha1 kind: App metadata: name: ros-v1 spec: components: cloud: chart: inline: robot: chart: inline: ``` Right now we only have bazel build rules to produce inline charts. ## AppRollout Resource An AppRollout describes how a defined App should be deployed across a fleet of clusters. It allows to flexibly select robots which should run an application and to inject fine-grained configuration. Example 3: AppRollout with different configuration options per target ```yaml apiVersion: apps.cloudrobotics.com/v1alpha1 kind: AppRollout metadata: name: ros-stable labels: app.kubernetes.io/name: ros role: navtest release: stable spec: appName: ros-v1 cloud: values: override: foo robots: - selector: matchLabels: model: mir100 values: override1: bar - selector: matchLabels: model: mir200 values: override1: baz version: v1.2.2 # Chart version override for canarying ``` AppRollouts are deployed into the cloud cluster, where a controller (app-rollout-controller) handles them. The controller applies the specified selectors and creates or updates the internal ChartAssignments. A ChartAssignment represents a single instance of a chart that should be installed into a single cluster. These internal objects describe the task of installing parts of the application. The federation layer will sync ChartAssignments to robots as needed. The actual installation is done by another controller (chart-assignment-controller), this time running both in the cloud and on the robots. The AppRollout controller will watch the status updates and consolidate the information into status updates on the AppRollout. ## Sharing secrets If you create a Secret in the `default` namespace labelled `cloudrobotics.com/copy-to-chart-namespaces=true`, it will be copied into all namespaces created by the chart-assignment-controller. This is useful for cluster-specific license keys that can be used by applications. ## Opt a pod out of status checking During rollout, the chart-assignment-controller checks for Pods in the rollout being `Running` or `Completed`. In some cases, this check is not necessary or might need to be opted out of. In this case, add a label `cloudrobotics.com/opt-out-error-checking=true` to your pods. Adding this instructs the chart-assignment-controller to not block the status from reaching `Ready`. ================================================ FILE: docs/concepts/config.md ================================================ # Project configuration The project configuration that one has entered during the initial setup is stored with the project in GCS. One can look at the options with the following command: ```shell gcloud storage cat gs://${PROJECT_ID}-cloud-robotics-config/config.sh ``` The settings contained in the config file are used by terraform to setup the project infrastructure and used by the cloud and chart-assignment-controller services running in kubernetes to configure apps. The terraform support is encapsulated in deploy.sh that creates a temporary `terraform.tfvars` file. To support configuring apps, we pass the settings to app-rollout-controller where they are provided as additional variables for helm templating. The command below prints the settings we pass to app-rollout-controller: ```shell kubectl get deployment app-rollout-controller -o=jsonpath='{.spec.template.spec.containers[0].args[0]}' ``` ================================================ FILE: docs/concepts/device_identity.md ================================================ # Device Identity Device Identity, part of Layer 1, provides an identity for robot clusters and services to integrate those identities into a cloud based IAM system. The following components are part of the whole setup: * Cloud: * `IAM`: Cloud Identity and Access Management * `Kubernetes configmaps`: used as a Key Management Service * `Token Vendor`: token exchange service for OAuth2 service accounts * `robot service-account`: a GCP IAM service account that has the union of permissions that applications running on the robot cluster require * Robot cluster (on-prem or edge): * `Metadata Server`: provides default credentials + project metadata * `Setup`: special app used to register the workcell * ``: any app accessing the cloud The following chapters explain the flows in more detail. Further information about the Token Vendor can be found in its [docs](https://github.com/googlecloudrobotics/core/tree/master/src/go/cmd/token-vendor/README.md) ## Setup The setup flow is used to register a new robot cluster to a cloud project. ![setup](device_identity_setup.png) * (1) (Admin-)user runs `Setup`, which generates a RSA key-pair and stores it as a K8S secret * (2) `Setup` uploads the public key to `Token Vendor` * (3) `Token Vendor` stores key in `Kubernetes` ## Authentication The authentication flow is used to transparently make cloud API calls work for on-prem robot clusters. ![setup](device_identity_auth.png) * `` creates an API client without loading any custom key material * (1) API client library probes `Metadata Server` to get ADCs (Application Default Credentials) * (2) `Metadata Server` talks to `Token Vendor` get an Access Token for the `robot service-account` * (3) `Token Vendor` verifies the key the request has been signed with against the device registry * (4) `Token Vendor` gets an Access Token for the `robot service-account` from `IAM` * `Token Vendor` returns Access Token through `Metadata Server` to the `` and that can use it to call Cloud APIs under the scope of the `robot service-account` ================================================ FILE: docs/concepts/federation.md ================================================ # Federation Federation, part of Layer 1, is responsible for synchronizing the state between robot and cloud clusters. Configuration state in Cloud Robotics Core is primarily expressed through [custom resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) by platform and user applications alike. Our federation system enables other components to use custom resources locally without needing to be aware of the multi cluster setup and the quality of the network connection. ## Semantics A Kubernetes resource is typically divided into a [“spec” and a “status”](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#object-spec-and-status) section. The `spec` section expresses the intent of the resource, typically authored by a user or another application along with all its metadata. The `status` section must generally only be written to by the controller that is responsible for realizing the specification. Consequently, `spec` and `status` typically each have one distinct author. For federating resources, this means that `spec` and `status` of a resource are owned by at most one cluster respectively (possibly the same one). The cluster owning the spec is also the main owner of the resource overall and controls its lifecycle, i.e. deletion. A resource’s spec is always synced from an upstream cluster to a downstream cluster and its status synced back from downstream to upstream. All resources of a specific type may either be synchronized to all robots or to exactly one robot. There is no direct synchronization between robots. However, a robot may create a resource in the cloud cluster that will be distributed to other robots. If a resource owned by the upstream cluster has been synchronized to one or more downstream clusters, it can only be permanently deleted upstream: if deleted downstream, it will be recreated. If deleted in the upstream cluster, it will be asynchronously deleted in other clusters that hold a copy of the resource. Upstream deletion can complete before the downstream resource is deleted. ## cr-syncer The cr-syncer component (Custom Resource Syncer) is a controller that runs inside each robot cluster. It is connected to the Kubernetes API servers of the cloud and the robot cluster alike and continuously watches for updates on custom resources. The controller contains retry and resync logic to address intermittent connectivity. ![federation](federation.png) The behavior of the cr-syncer can be configured per custom resource definition (CRD) by setting annotations on its CRD: * `cr-syncer.cloudrobotics.com/spec-source`: may be `cloud` or `robot`. It determines which cluster type owns metadata, spec, and lifecycle of all resources of the CRD. It implies that the other cluster type owns the status section. * `cr-syncer.cloudrobotics.com/filter-by-robot-name`: a boolean that determines whether resources will be synced to all robots or just a single one. An individual resource is labeled with `cloudrobotics.com/robot-name` to indicate which robot it should be synced to. If the label is missing on a resource, it will not be synced at all. * `cr-syncer.cloudrobotics.com/status-subtree`: a string key, which defines which sub-section of the resource status is synced from the downstream cluster. This lets you split a resource’s status into `robot` and `cloud` sections, for example. Using this annotation is generally discouraged as it likely points to a flaw in the modeling of the respective CRD. ## Deletion When the cr-syncer sees a resource in the downstream cluster with no corresponding resource in upstream cluster, it deletes it. This handles orphaned resources when the upstream resource was deleted while the cr-syncer was restarting. It also means that you can't create a resource directly in the downstream cluster. The upstream resource is identified using the namespace and name, but not the UID, so deletion and recreation upstream may result in an update in the downstream cluster. > **Note**: if you create a resource directly in the downstream cluster, the > behavior will depend on how the CRD is annotated. If `filter-by-robot-name` > is false, the cr-syncer will delete all downstream resources that don't > correspond to upstream resources. This means that by listing CRs in the > upstream cluster, you can reason about which CRs will exist in the downstream > cluster. > > If `cr-syncer.cloudrobotics.com/filter-by-robot-name` is true, then the > cr-syncer will ignore any downstream resources that are not labelled with a > matching robot name. This means that a robot can run ChartAssignments that are > synced from the cloud as well as those created directly in the robot cluster. In some cases, downstream deletion may be blocked. For example, if we have deleted an upstream ChartAssignment, but the chart-assignment-controller has failed to remove its finalizer from the downstream ChartAssignment. This edge case leads to surprising behavior: - An upstream ChartAssignment can be recreated before the downstream ChartAssignment is deleted. - The old status from the downstream cluster will be synced to the new upstream ChartAssignment. If needed, this can be detected by watching the downstream cluster after deleting the resource from the downstream cluster. The situation will clean up once downstream deletion is complete. Note: previously, the cr-syncer used finalizers to block upstream deletion until the downstream resource was deleted. This gave the original deleter more information: for example, once an AppRollout had been deleted in the cloud, it means that all robots have terminated the app's pods. However, this caused problems with offline or renamed clusters: an admin would have to manually clean up the old finalizers. The new asynchronous behavior is not affected by offline clusters. ## Resource generations Custom resources have a field `.metadata.generation` that starts at 1 and is incremented when the resource changes. Specifically, if the CRD enables the /status subresource, the generation increases by 1 every time the resource spec changes, but not when the status changes. The resource controller can set `.status.observedGeneration` to the latest generation it has observed, so the user can change the spec, then wait for `observedGeneration` to catch up before looking at the status. For example: * Create a Deployment for one pod (generation=1), and wait for the status to be Ready. * Change the Deployment's image reference (generation=2): the status is still Ready, but this refers the old spec (observedGeneration=1). * Wait for the status to update (observedGeneration=2): now the status is non-ready, referring to the newer spec. * Wait for the status to be Ready. The new image is now running. `generation` and `observedGeneration` can **only be compared in the downstream cluster**. As the generation is managed by the Kubernetes apiserver, the cr-syncer cannot guarantee that the upstream generation matches the downstream generation. On the other hand, `observedGeneration` will be copied from downstream to upstream with the rest of `.status`. This means that `generation` is cluster-specific but `observedGeneration` always refers to the downstream generation. ================================================ FILE: docs/developers/debug-auth.md ================================================ # Debugging authentication problems Useful tips for working with Authentication and Authorization systems. ## Run a sample request with various credentials You can call Cloud APIs with curl to see whether authorization works. ### Your own credentials ```bash PROJECT_NUMBER=201199916163 curl -v -H "Content-Type: application/json" \ -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \ "https://cloudroboticssensordata.googleapis.com/v1eap/projects/${PROJECT_NUMBER}/sensors" ``` ### Service account JSON file You can create a JSON file with the robot account's credentials on the [Cloud console's credentials page](https://console.cloud.google.com/apis/credentials). ```bash PROJECT_NUMBER=201199916163 JSON_CREDENTIALS=/tmp/my-project-b7364a68fa92.json curl -v -H "Content-Type: application/json" \ -H "Authorization: Bearer $(GOOGLE_APPLICATION_CREDENTIALS=${JSON_CREDENTIALS} gcloud auth application-default print-access-token)" \ "https://cloudroboticssensordata.googleapis.com/v1eap/projects/${PROJECT_NUMBER}/sensors" ``` ### Get an OAuth token from IAM The token vendor doesn't have its own keys, but instead calls IAM's generateAccessToken method. You can emulate its behavior by using the [API Explorer](https://developers.google.com/apis-explorer/#search/iam%20credentials/iamcredentials/v1/iamcredentials.projects.serviceAccounts.generateAccessToken) to call `iamcredentials.projects.serviceAccounts.generateAccessToken`. The name parameter is `projects/-/serviceAccounts/robot-service@my-project.iam.gserviceaccount.com`, and the `scope` is `https://www.googleapis.com/auth/cloud-platform`. You can pass the returned access token in an Authorization header as above. ## Check whether the client's request is well-formed and authenticated The easiest way to verify that metadata server and the gRPC client library are doing the right thing is to use a logging HTTP server as the gRPC server. Instead of setting the gRPC host to the Cloud API server (`cloudroboticssensordata.googleapis.com`), you set it to an HTTPS-capable server under your control. You need HTTPS support because otherwise the gRPC library will rightfully decline to send access tokens. Luckily, your Cloud Robotics Core setup already runs an HTTPS server. Suppose you're calling the gRPC service `google.cloud.robotics.sensordata.v1eap.SensorDataService`. You can hook a very simple Python HTTP server into your cloud nginx setup like this: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: debug spec: server.py: | import BaseHTTPServer import SocketServer class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): print "Got request for ", self.path, " with auth ", self.headers.get('Authorization') def do_POST(self): print "Got request for ", self.path, " with auth ", self.headers.get('Authorization') httpd = SocketServer.TCPServer(("", 8080), MyHandler) httpd.serve_forever() --- apiVersion: apps/v1 kind: Deployment metadata: name: debug spec: selector: matchLabels: app: debug replicas: 1 template: metadata: labels: app: debug spec: containers: - name: python image: python:2 args: ["python", "/src/server.py"] volumeMounts: - name: src-volume mountPath: /src volumes: - name: src-volume configMap: name: debug --- apiVersion: v1 kind: Service metadata: labels: app: debug name: debug spec: ports: - name: http port: 8082 protocol: TCP targetPort: 8080 selector: app: debug type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/backend-protocol: HTTP name: debug spec: ingressClassName: nginx rules: - host: www.endpoints.my-project.cloud.goog http: paths: - path: /google.cloud.robotics.sensordata.v1eap.SensorDataService pathType: Prefix backend: service: name: debug port number: 8080 ``` This will log the Authorization header to the pod's stdout, so you can view it with `kubectl logs`. Save the token to a file (don't paste it into the command line because it will end up in your shell history). ## Checking tokens with the tokeninfo service You can check the token's contents and sanity with Google's tokeninfo endpoint: ```shell curl https://oauth2.googleapis.com/tokeninfo?access_token=$(cat /tmp/token.txt) ``` ================================================ FILE: docs/how-to/connecting-robot.md ================================================ # Connecting a robot to the cloud Estimated time: 10 min This page describes how to connect a Kubernetes cluster on a robot running Ubuntu 20.04 to the cloud. Once you've done this, you can: * Run a private Docker container from the Google Container Registry * Securely communicate with cloud services * See logs from the robot in the Cloud Console ## Setting up the GCP project 1. If you haven't already, complete the [Setting up the GCP project](../quickstart.md) steps. 1. On the computer you used to set up the cloud project, generate an access token, which you'll use to give the robot access to the cloud: ```shell gcloud auth application-default print-access-token ``` > **Note:** If you want to reduce the risk that your cloud project is > compromised using this token during its 1h lifetime, you can generate a less > privileged service account token: > > ``` > SA="human-acl@${PROJECT_ID}.iam.gserviceaccount.com" > gcloud iam service-accounts add-iam-policy-binding "${SA}" \ > --role=roles/iam.serviceAccountTokenCreator \ > --project="${PROJECT_ID}" --member="user:${YOUR_EMAIL_ADDRESS:?}" > gcloud auth print-access-token --impersonate-service-account="${SA}" > ``` > > If you see `ERROR: Failed to impersonate ...`, wait a few minutes for the IAM > policy to propagate. > > You can ignore the "WARNING: This command is using service account > impersonation." ## Installing the cluster on the robot ## Installing Kubernetes You'll need to install a Kubernetes cluster on the robot before you can connect it to the cloud. The cluster manages and supports the processes that communicate with the cloud. Please see external references for setting up k8s. For simplicity we recommend [k3s](https://k3s.io/) or a single node [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/) cluster (untested). ## Setting up the robot 1. Set up the robot cluster to connect to the cloud. When running `setup_robot.sh`, you'll need to enter the access token you generated earlier. You may find it easiest if you SSH into the robot from the workstation you used to set up the project. ```shell mkdir -p ~/cloud-robotics-core cd ~/cloud-robotics-core curl https://raw.githubusercontent.com/googlecloudrobotics/core/master/src/bootstrap/robot/setup_robot.sh >setup_robot.sh bash setup_robot.sh my-robot --project ${PROJECT_ID} \ --robot-type my-robot-type ``` Set `${PROJECT_ID}` to your GCP project ID. When prompted for an access token, provide the authentication token you generated earlier. > **Note:** `my-robot-type` is a placeholder and you can ignore it for now. ## What's next * [Using Cloud Storage from a robot](using-cloud-storage.md). ================================================ FILE: docs/how-to/creating-declarative-api.md ================================================ # Creating a declarative API In this guide we will use a Kubernetes-style declarative API to interface to an external Charge Service for a robot. This API is built around the concept of a ChargeAction resource, which instructs a robot to drive to a charger. While the robot is charging, the status of the ChargeAction resource is kept up-to-date and can be observed. ## Motivation RPC-based systems like ROS's [actionlib](http://wiki.ros.org/actionlib), while proven to be scalable, maintainable and useful, leave a few things to be desired: 1. **Synchronization**. The intent for a controller is stored in-memory in multiple components and we rely on correct synchronization. For example, the motion planner sends the "turn wheel 3 times per second" message to the wheel actuator, then trusts that the wheel actuator will have received the intent and waits for it to act on the shared intent. If a second process (such as an emergency stop) overwrites the intent of the wheel actuator, there's no standard channel to notify the motion planner. 2. **Persistence**. Since the intent is stored in-memory, it is lost when any process restarts. This is the core reason that software in ROS systems can't be updated on the fly. 3. **Inspection**. For debugging, a coherent view into the current system intent would be great. In RPC-based APIs, the intent is often updated differentially (eg "a little more to the left"), so our only hope of debugging is to log all messages ever sent. In a declarative API, all actions and feedback are stored in a shared database—an approach built on Kubernetes' experience building robust distributed systems—which addresses these issues. The latency added by going through the shared database means that declarative APIs are best suited to latency-tolerant applications like high-level control. ## Prerequisites * Completed the [Quickstart Guide](../quickstart.md), after which the GCP project is set up and `gcloud-sdk` and `kubectl` are installed and configured. * `docker` is installed and configured on the workstation ([instructions](https://docs.docker.com/install/linux/docker-ce/ubuntu/)). Create a directory for the code examples of this guide, e.g.: ```shell mkdir charge-service cd charge-service ``` Set your GCP project ID as an environment variable: ``` export PROJECT_ID=[YOUR_GCP_PROJECT_ID] ``` All files created in this tutorial can be found in [docs/how-to/examples/charge-service](https://github.com/googlecloudrobotics/core/tree/master/docs/how-to/examples/charge-service). If you download the files, you have to replace the placeholder `[PROJECT_ID]` with your GCP project ID: ```shell sed -i "s/\[PROJECT_ID\]/$PROJECT_ID/g" charge-controller.yaml ``` ## Installing metacontroller This tutorial is based on [metacontroller](https://metacontroller.github.io/metacontroller/intro.html), an add-on for Kubernetes that makes it easy to write and deploy [custom controllers](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#custom-controllers). Custom controllers implement the logic behind a declarative API. First, make sure that `kubectl` points to the correct GKE cluster: ```shell kubectl config get-contexts ``` If the correct cluster is not marked with an asterisk in the output, you can switch to it with `kubectl config use-context [...]`. Now install metacontroller to the cloud-cluster: ```shell kubectl create namespace metacontroller kubectl apply -f https://raw.githubusercontent.com/metacontroller/metacontroller/master/manifests/production/metacontroller-rbac.yaml kubectl apply -f https://raw.githubusercontent.com/metacontroller/metacontroller/master/manifests/production/metacontroller-crds-v1.yaml kubectl apply -f https://raw.githubusercontent.com/metacontroller/metacontroller/master/manifests/production/metacontroller.yaml ``` Let's check that all resources came up: ```console > kubectl get pods --namespace metacontroller NAME READY STATUS RESTARTS AGE metacontroller-0 1/1 Running 0 1m ``` You can learn more details in the metacontroller's [install instructions](https://metacontroller.github.io/metacontroller/guide/install.html). > **Limitations of metacontroller**: > Writing custom controllers with metacontroller is easy, and you can use > whatever programming language you prefer. > However, it has some limitations. > > 1. metacontroller can't directly detect changes in external state, although > you can configure it to periodically reconcile your resources with external > systems. This introduces latency corresponding to the reconciliation > frequency. > 1. The information available to your controller is limited. > You can't use metacontroller to create a controller that only acts on a > single resource out of many (for example, a controller that only executes > the highest-priority action). > > For advanced use cases, writing a controller in Golang offers more > flexibility. See > [sample-controller](https://github.com/kubernetes/sample-controller) for an > example. ## Defining the controller logic The core of a declarative API is the controller logic, which defines how the resources should be handled and reports the current status. For the Charge Service, we've implemented the logic in a Python script. Download [server.py](examples/charge-service/server.py): ```shell curl -O https://raw.githubusercontent.com/googlecloudrobotics/core/master/docs/how-to/examples/charge-service/server.py ``` This Python program implements a server that listens on port 80 for incoming HTTP POST requests from metacontroller. The controller logic is contained in the `sync()` method, which handles new ChargeActions by calling `charge_service.start_charging()`, and handles in-progress ChargeActions by updating the status. [embedmd]:# (examples/charge-service/server.py python /.*state = current_status.get/ /return.*status.*/) ```python state = current_status.get("state", "CREATED") if state == "CREATED": # The ChargeAction has just been created. Use the external Charge Service # to start charging. Store the request ID in the status so we can use it # to check the state of the charge request. request_id = self.charge_service.start_charging() desired_status["state"] = "IN_PROGRESS" desired_status["request_id"] = request_id elif state == "IN_PROGRESS": try: # Get the progress of the charge request from the external service. progress = self.charge_service.get_progress( current_status["request_id"]) desired_status["charge_level_percent"] = progress if progress == 100: # Charging has completed. desired_status["state"] = "OK" except ValueError as e: # The charge request was not found. This could be because the robot was # restarted during a charge, and the request was forgotten. desired_status["state"] = "ERROR" desired_status["message"] = str(e) elif state in ["OK", "CANCELLED", "ERROR"]: # Terminal state, do nothing. pass else: desired_status["state"] = "ERROR" desired_status["message"] = "Unrecognized state: %r" % state return {"status": desired_status, "children": []} ``` ## Dockerizing the service Next, to prepare our controller logic for deployment in the cloud, we package it as a Docker image. Make sure that the docker daemon is running and that your user has the necessary privileges: ```shell docker run --rm hello-world ``` If this command fails, make sure Docker is installed according to the [installation instructions](https://docs.docker.com/install/linux/docker-ce/ubuntu/). In the same directory as `server.py`, create a `Dockerfile` with the following contents: [embedmd]:# (examples/charge-service/Dockerfile dockerfile) ```dockerfile FROM python:alpine WORKDIR /data COPY server.py ./ CMD [ "python", "-u", "./server.py" ] ``` (Note: the `-u` option disables line-buffering; Python's line-buffering can prevent output from appearing immediately in the Docker logs.) To build the Docker image, run: ```shell docker build -t charge-controller . ``` You should see `Successfully tagged charge-controller:latest`. You can run the container locally with: ```shell docker run -ti --rm -p 8000:8000 charge-controller ``` Then, from another terminal on the same workstation, send a request with an empty `parent` object: ```shell curl -X POST -d '{"parent": {}, "children": []}' http://localhost:8000/ ``` You should see a response like: ```json {"status": {"state": "IN_PROGRESS", "request_id": "2423e70c-9dc7-47ac-abcb-b2ef0cbc676c"}, "children": []} ``` The response indicates that the controller would set `"state": "IN_PROGRESS"` on a newly-created ChargeAction. ## Uploading the Docker image to the cloud In order to be able to run the server as a container in our cloud cluster, we need to upload the Docker image to our GCP project's private [container registry](https://cloud.google.com/container-registry/docs/pushing-and-pulling). Enable the Docker credential helper: ```shell gcloud auth configure-docker ``` Tag the image and push it to the registry: ```shell docker tag charge-controller gcr.io/$PROJECT_ID/charge-controller docker push gcr.io/$PROJECT_ID/charge-controller ``` The image should now show up in the [Container Registry](https://console.cloud.google.com/gcr). ## Deploying the declarative API in the cloud Create a file called `charge-crd.yaml` with the following contents: [embedmd]:# (examples/charge-service/charge-crd.yaml yaml) ```yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: chargeactions.example.com annotations: cr-syncer.cloudrobotics.com/spec-source: cloud spec: group: example.com names: kind: ChargeAction plural: chargeactions singular: chargeaction scope: Namespaced versions: - name: v1 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object x-kubernetes-preserve-unknown-fields: true --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:cr-syncer:chartaction labels: cr-syncer.cloudrobotics.com/aggregate-to-robot-service: "true" rules: - apiGroups: - example.com resources: - chargeactions verbs: - get - list - watch - update ``` This is a [custom resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) definition (CRD) for a resource called ChargeAction. This simple example just describes the name and version of the API, but CRDs can also define schemas for the resources. The ClusterRole configures [role-based access control](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) to let the robot access the ChargeActions. Don't worry about the `cr-syncer.cloudrobotics.com/spec-source` annotation for now, as it'll be explained later in the tutorial. Next, create a file called `charge-controller.yaml` with the following contents, replacing `[PROJECT_ID]` with your GCP project ID: [embedmd]:# (examples/charge-service/charge-controller.yaml yaml) ```yaml apiVersion: metacontroller.k8s.io/v1alpha1 kind: CompositeController metadata: name: charge-controller spec: generateSelector: true parentResource: apiVersion: example.com/v1 resource: chargeactions resyncPeriodSeconds: 1 hooks: sync: webhook: url: http://charge-controller.default:8000/sync --- apiVersion: v1 kind: Service metadata: name: charge-controller spec: selector: app: charge-controller ports: - port: 8000 --- apiVersion: apps/v1 kind: Deployment metadata: name: charge-controller spec: replicas: 1 selector: matchLabels: app: charge-controller template: metadata: labels: app: charge-controller spec: containers: - name: controller image: gcr.io/[PROJECT_ID]/charge-controller ports: - containerPort: 8000 ``` This file contains the information needed by Kubernetes and metacontroller to handle ChargeAction resources. In the following, we will go over it bit by bit assuming basic familiarity with the [YAML format](https://en.wikipedia.org/wiki/YAML). We define three Kubernetes resources: * The *CompositeController* tells metacontroller to send ChargeAction resources to the charge-controller Service. * The *Service* defines how the HTTP server is exposed within the cluster. * The *Deployment* describes the Docker container to run. Metadata, labels, and selectors are used to tie the three resources together. A detailed explanation of the Kubernetes resources is out of scope for this guide, check out the [Kubernetes docs](https://kubernetes.io/docs/home/) or [metacontroller User Guide](https://metacontroller.github.io/metacontroller/guide.html) to get started. There are a few points worth mentioning, though: * In the Deployment, don't forget to replace `[PROJECT_ID]` with your GCP project ID. * The CompositeController sets `resyncPeriodSeconds: 1`. This tells metacontroller to check each ChargeAction every second. This allows `server.py` to update the progress every second while the action is in progress. * The CompositeController sets `url: http://charge-controller.default:8000/sync`. This tells metacontroller that the ChargeAction resources are handled by a service called `charge-controller` in the `default` namespace. Deploy these resources by applying the configuration: ```shell kubectl apply -f charge-crd.yaml kubectl apply -f charge-controller.yaml ``` You can explore the various resources that were created on your cluster as a result of this command in the [GKE Console](https://console.cloud.google.com/kubernetes/workload) or with `kubectl`, e.g.: ```shell kubectl get pods ``` The resulting list should contain a running pod with a name like `charge-controller-xxxxxxxxxx-xxxxx`. ## Redeploying after a change If you make a change to `server.py`, you need to rebuild and push the Docker image: ```shell docker build -t charge-controller . docker tag charge-controller gcr.io/$PROJECT_ID/charge-controller docker push gcr.io/$PROJECT_ID/charge-controller ``` The easiest way to get Kubernetes to restart the workload with the latest version of the container is to delete the pod: ```shell kubectl delete pod -l 'app=charge-controller' ``` Kubernetes will automatically pull the newest image and recreate the pod. If you make a change to `charge-controller.yaml`, all you have to do is apply it again: ```shell kubectl apply -f charge-controller.yaml ``` ## Accessing the API You can use `kubectl` to interact with the API. Create a file called `charge-action.yaml` with the following contents: [embedmd]:# (examples/charge-service/charge-action.yaml yaml) ```yaml apiVersion: example.com/v1 kind: ChargeAction metadata: name: my-charge-action ``` Run the following command to create a ChargeAction and observe how its status changes: ```shell kubectl apply -f charge-action.yaml \ && watch -n0 kubectl describe chargeaction my-charge-action ``` Over the next 10 seconds, you should see the "Charge Level Percent" increase to 100, and then the state should become "CHARGED". > **Troubleshooting**: > If the ChargeAction's status doesn't change, check that metacontroller is installed by running `kubectl --namespace metacontroller get pods`. > You should see `metacontroller-0 1/1 Running`. > You can also check the metacontroller logs with `kubectl --namespace metacontroller logs metacontroller-0` ## Deploying the declarative API on the robot. So far, the Charge Service has been running in the cloud, but we need to run code on the robot to get it to charge. We can change this with the `cr-syncer`, a component of Cloud Robotics Core that allows declarative APIs to work between Kubernetes clusters. In particular, we can run the charge-controller on the robot, while creating the ChargeAction in the cloud cluster. The `cr-syncer` takes care of copying the ChargeAction to the robot when the robot has network connectivity. **Prerequisite**: you'll need a robot that has been successfully [connected to the cloud](connecting-robot.md). First, remove the controller from the cloud cluster: ```shell # Note: run this on the workstation kubectl delete -f charge-controller.yaml ``` Then SSH into the robot, install metacontroller, and bring up the charge-controller there: ```shell # Note: run this on the robot kubectl create namespace metacontroller kubectl apply -f https://raw.githubusercontent.com/metacontroller/metacontroller/master/manifests/production/metacontroller-rbac.yaml kubectl apply -f https://raw.githubusercontent.com/metacontroller/metacontroller/master/manifests/production/metacontroller-crds-v1.yaml kubectl apply -f https://raw.githubusercontent.com/metacontroller/metacontroller/master/manifests/production/metacontroller.yaml export PROJECT_ID=[YOUR_GCP_PROJECT_ID] kubectl apply -f https://raw.githubusercontent.com/googlecloudrobotics/core/master/docs/how-to/examples/charge-service/charge-crd.yaml curl https://raw.githubusercontent.com/googlecloudrobotics/core/master/docs/how-to/examples/charge-service/charge-controller.yaml \ | sed "s/\[PROJECT_ID\]/$PROJECT_ID/g" | kubectl apply -f - ``` Now, check that these are running correctly: ```console # Note: run this on the robot > kubectl get pods --namespace metacontroller NAME READY STATUS RESTARTS AGE metacontroller-0 1/1 Running 0 1m > kubectl get pods -l app=charge-controller NAME READY STATUS RESTARTS AGE charge-controller-57786849f8-xp5kf 1/1 Running 0 77s ``` Switch back to a terminal on your workstation. As before, you can create a ChargeAction with `kubectl`, but this time it will be handled by the controller on the robot. ```shell # Note: run this on the workstation kubectl delete -f charge-action.yaml kubectl apply -f charge-action.yaml \ && watch -n0 kubectl describe chargeaction my-charge-action ``` How does this work? - The `cr-syncer` runs on the robot and watches custom resources in the cloud. - It sees the `cr-syncer.cloudrobotics.com/spec-source: cloud` annotation on the CustomResourceDefinition, which tells it to copy the `spec` from `my-charge-action` in the cloud cluster into a copy of `my-charge-action` in the robot cluster. - While the robot is charging, the robot's charge-controller updates the status in the robot's cluster. - The `cr-syncer` copies the status back up to the original resource in the cloud cluster. ## Cleaning up In order to stop the controller and remove the CRD you created, run: ```shell kubectl delete -f charge-controller.yaml -f charge-crd.yaml ``` If you want to uninstall metacontroller too, run: ```shell kubectl delete namespace metacontroller ``` If you installed on the robot, you'll need to run these commands there too. ================================================ FILE: docs/how-to/deploy-from-sources.md ================================================ # Deploy Cloud Robotics Core from sources Estimated time: 30 min This page describes how to set up a Google Cloud Platform (GCP) project containing the Cloud Robotics Core components. In particular, this creates a cluster with Google Kubernetes Engine and prepares it to accept connections from robots, which enables those robots to securely communicate with GCP. The commands were tested on machines running Debian (Stretch) or Ubuntu (16.04 and 18.04) Linux. 1. In the GCP Console, go to the [Manage resources][resource-manager] page and select or create a project. 1. Make sure that [billing][modify-project] is enabled for your project. 1. [Install the Cloud SDK][cloud-sdk]. When prompted, choose the project that you created above. 1. After installing the Cloud SDK, install the `kubectl` command-line tool and the GKE auth plugin: ```shell gcloud components install kubectl gcloud components gke-gcloud-auth-plugin ``` If you're using Debian or Ubuntu, you may need to use `apt install kubectl` instead. 1. [Install the Bazel build system][install-bazel]. 1. Install additional build dependencies: ```shell sudo apt-get install default-jdk git python-dev unzip xz-utils ``` [resource-manager]: https://console.cloud.google.com/cloud-resource-manager [modify-project]: https://cloud.google.com/billing/docs/how-to/modify-project [cloud-sdk]: https://cloud.google.com/sdk/docs/ [install-bazel]: https://github.com/bazelbuild/bazel/blob/4.0.0/site/docs/install-ubuntu.md ## Build and deploy the project 1. Clone the source repo. ```shell git clone https://github.com/googlecloudrobotics/core cd core ``` 1. Create application default credentials, which are used to deploy the cloud project and authorize access to the cloud docker registry. ```shell gcloud auth application-default login gcloud auth configure-docker ``` 1. Create a Cloud Robotics config in your project: ```shell ./deploy.sh set_config [PROJECT_ID] ``` You can keep the defaults for the other settings by hitting `ENTER`. This command creates a file `config.sh` containing your choices and stores in into a cloud-storage bucket named `[PROJECT_ID]-cloud-robotics-config`. You can verify the settings using: ```shell gcloud storage cat gs://[PROJECT_ID]-cloud-robotics-config/config.sh ``` 1. Build the project. Depending on your computer and internet connection, it may take around 15 minutes. ```shell bazel build //... ``` 1. Deploy the cloud project. ```shell ./deploy.sh create [PROJECT_ID] ``` > **Known issue:** > Sometimes, this command fails with an error message like > `Error 403: The caller does not have permission`, > `Error 403: Service ... not found or permission denied', > `Bad status during token exchange: 503`, or > `Error enabling service`. > In these cases, wait for a minute and try again. `deploy.sh` created a Kubernetes cluster using Google Kubernetes Engine and used Helm to install the Cloud Robotics Core components. You can browse these components on the [Workloads dashboard](https://console.cloud.google.com/kubernetes/workload). Alternatively, you can list them from the console on your workstation: ```console $ kubectl get pods NAME READY STATUS RESTARTS AGE cert-manager-xxx 1/1 Running 0 1m nginx-ingress-xxx 1/1 Running 0 1m oauth2-proxy-xxx 0/1 CrashLoopBackOff 4 1m token-vendor-xxx 1/1 Running 0 1m ``` > **Note** Unless you already set up OAuth, the `oauth2-proxy` will show an error which we will ignore for now. In addition to the cluster, `deploy.sh` also created: * the [[PROJECT_ID]-robot Cloud Storage bucket](https://console.cloud.google.com/storage/browser), containing the scripts that connect robots to the cloud, and * the [Identity & Access Management policies](https://console.cloud.google.com/iam-admin/iam) that authorize robots and humans to communicate with GCP. With the project deployed, you're ready to [connect a robot to the cloud](connecting-robot.md). ## Update the project To apply changes made in the source code, run: ```shell ./deploy.sh update [PROJECT_ID] ``` ## Clean up The following command will delete: * the [cloud-robotics Kubernetes cluster](https://console.cloud.google.com/kubernetes/list) This can be useful if the cluster is in a broken state. Be careful with this invocation, since you'll have to redeploy the project and reconnect any robots afterwards. ```shell ./deploy.sh delete [PROJECT_ID] ``` If you want to completely shut down the project, see [the Resource Manager documentation](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects). ## What's next * [Connecting a robot to the cloud](connecting-robot.md). * [Setting up OAuth for web UIs](setting-up-oauth.md). ================================================ FILE: docs/how-to/deploying-grpc-service.md ================================================ # Deploying a gRPC service written in C++ Estimated time: 60 min In this guide we will deploy a gRPC service written in C++ and deploy it to our Google Kubernetes Engine (GKE) cluster in the cloud in such a way that authentication is required for access. We will show how to access the service from the workstation and how to access it from code running in the robot's Kubernetes cluster. ## Prerequisites * Completed the [Quickstart Guide](../quickstart.md), after which the GCP project is set up and `gcloud-sdk` and `kubectl` are installed and configured. * `docker` is installed and configured on the workstation ([instructions](https://docs.docker.com/install/linux/docker-ce/ubuntu/)). * `git` is installed on the workstation. * For the last part of the guide: A robot that has been successfully [connected to the cloud](connecting-robot.md). All files for this tutorial are located in [docs/how-to/examples/greeter-service/](https://github.com/googlecloudrobotics/core/tree/master/docs/how-to/examples/greeter-service). ```shell git clone https://github.com/googlecloudrobotics/core cd core/docs/how-to/examples/greeter-service ``` Set your GCP project ID as an environment variable: ```shell export PROJECT_ID=[YOUR_GCP_PROJECT_ID] ``` ## Running gRPC server and client locally We will use [gRPC's quickstart example](https://grpc.io/docs/quickstart/cpp.html) with small modifications. If you like to learn more about gRPC in C++, follow their guide first. The gRPC `helloworld.Greeter` service is defined in `proto/helloworld.proto`. It accepts a `HelloRequest` containing a `name` and responds with a `HelloReply` containing a `message`. The server is implemented in `server/server.cc` and the client is implemented `client/client.cc`. The client sends the request with `name: "world"` to the server which responds with `message: "Hello "`. In this tutorial, we build the server and client code inside Docker containers, so you don't need to install the gRPC library. If you prefer, you can install the gRPC following [these instructions](https://github.com/grpc/grpc/blob/master/src/cpp/README.md) and build the server and client locally using the provided `Makefile`. Make sure the Docker daemon is running and your user has the necessary privileges: ```shell docker run --rm hello-world ``` If this command fails, make sure Docker is installed according to the [installation instructions](https://docs.docker.com/install/linux/docker-ce/ubuntu/). The Docker image for the server is configured in `server/Dockerfile`: [embedmd]:# (examples/greeter-service/server/Dockerfile dockerfile) ```dockerfile FROM grpc/cxx:1.12.0 WORKDIR /data COPY server/server.cc ./server/ COPY proto/helloworld.proto ./proto/ COPY Makefile ./ RUN make greeter-server && make clean CMD ["./greeter-server"] ``` We use the [grpc/cxx](https://hub.docker.com/r/grpc/cxx) Docker image which contains all the build tools and libraries (`g++`, `make`, `protoc`, and `grpc`) we need to build the `greeter-server` binary. The Docker image for the client is configured in `client/Dockerfile` which builds the `greeter-client` from `client/client.cc`. To build the Docker images, run: ```shell docker build -t greeter-server -f server/Dockerfile . docker build -t greeter-client -f client/Dockerfile . ``` > **Note** > The docker files are in the subfolders `greeter-server/server/` and `greeter-server/client`, but the docker command must be called from `greeter-server/` to include the files which are shared between the server and the client. You should now have an image tagged `greeter-server` and one tagged `greeter-client` in your local registry: ```shell docker images | grep greeter ``` To run the server locally, the container's port 50051, which specified as gRPC port in `server.cc`, has to be published to your machine with the flag `-p 50051:50051`: ```shell docker run --rm -p 50051:50051 --name greeter-server greeter-server ``` In another console run the client container. The flag `--network=host` tells the container to use your workstation's network stack which allows the client to connect to `localhost`. ```shell docker run --rm --network=host greeter-client ./greeter-client localhost ``` You should see `Greeter received: Hello world` in the client's output and `Received request: name: "world"` in the server's output. You can also send your own name in the gRPC request to the server, try: ```shell docker run --rm --network=host greeter-client \ ./greeter-client localhost $USER ``` You can stop the server from another terminal by running: ```shell docker stop greeter-server ``` ## Uploading the Docker image to the cloud In order to be able to run the server as a container in our cloud cluster, we need to upload the Docker image to our GCP project's private [container registry](https://cloud.google.com/container-registry/docs/pushing-and-pulling). Enable the Docker credential helper: ```shell gcloud auth configure-docker ``` Tag the image and push it to the registry: ```shell docker tag greeter-server gcr.io/$PROJECT_ID/greeter-server docker push gcr.io/$PROJECT_ID/greeter-server ``` The image should now show up in the [container registry](https://console.cloud.google.com/gcr). ## Deploying the service in the cloud using Kubernetes Run the following command to create `greeter-server.yaml` using the provided template: ```shell cat greeter-server.yaml.tmpl | envsubst >greeter-server.yaml ``` This file contains the information needed by Kubernetes to run the gRPC service in our cloud cluster. The three resources, Ingress, Service, and Deployment, are explained in the [deploying a service tutorial](deploying-service.md). In contrast to the other tutorial, the Ingress tells nginx to forward incoming requests to a gRPC backend. [embedmd]:# (examples/greeter-service/greeter-server.yaml.tmpl yaml /^/ /---/) ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: greeter-server-ingress annotations: nginx.ingress.kubernetes.io/backend-protocol: GRPC nginx.ingress.kubernetes.io/auth-url: "http://token-vendor.default.svc.cluster.local/apis/core.token-vendor/v1/token.verify?robots=true" spec: ingressClassName: nginx rules: - host: "www.endpoints.${PROJECT_ID}.cloud.goog" http: paths: # must match the namespace and service name in the proto - path: /helloworld.Greeter/ pathType: Prefix backend: service: name: greeter-server-service # must match the port used in server.cc port: number: 50051 --- ``` Make sure that `kubectl` points to the correct GCP project: ```shell kubectl config get-contexts ``` If the correct cluster is not marked with an asterisk in the output, you can switch contexts with `kubectl config use-context [...]`.) Then deploy by applying the configuration: ```shell kubectl apply -f greeter-server.yaml ``` You can explore the various resources that were created on your cluster as a result of this command in the [GKE Console](https://console.cloud.google.com/kubernetes/workload) or with `kubectl`, e.g.: ```shell kubectl get pods ``` The resulting list should contain a running pod with a name like `greeter-server-xxxxxxxxxx-xxxxx`. ## Redeploying after a change For convenience, `deploy.sh` provides some commands to create, delete, and update the service. If you make changes to `greeter-server.yaml.tmpl`, all you have to do is run: ```shell ./deploy.sh update_config ``` If you make changes to `server.cc`, you need to run: ```shell ./deploy.sh update_server ``` This builds, tags, and pushes the Docker image, and then forces a redeployment of the image by calling `kubectl delete pod -l 'app=greeter-server-app'`. It also updates the resource definitions, so you don't have to run `./deploy.sh update_config` if you made changes to both files. ## Accessing the API In `client/client.cc` we use `grpc::InsecureChannelCredentials()` when talking to `localhost` while we use `grpc::GoogleDefaultCredentials()` when talking to any other address. SSL authentication with credentials from the user or robot are necessary when talking to the `greeter-server` in the Cloud Robotics project. [embedmd]:# (examples/greeter-service/client/client.cc c++ /^ +if.*localhost/ /^ +}$/) ```c++ if (grpc_endpoint.find("localhost:") == 0 || grpc_endpoint.find("127.0.0.1:") == 0) { channel_creds = grpc::InsecureChannelCredentials(); } else { channel_creds = grpc::GoogleDefaultCredentials(); } ``` Let's try to access our server. We have to connect to the nginx ingress which is hosted on `www.endpoints.$PROJECT_ID.cloud.goog:443`. To ensure we have valid credentials to talk to nginx we have to mount our `~/.config` folder in the container. ```shell docker run --rm -v ~/.config:/root/.config greeter-client \ ./greeter-client www.endpoints.$PROJECT_ID.cloud.goog:443 workstation ``` Recall that when running `./greeter-server` on your workstation you were able to see the server's log output upon receiving a request. This log output is also recorded when the server is running in the cloud cluster. To inspect it, run: ```shell kubectl logs -l 'app=greeter-server-app' ``` Or go to the [GKE Console](https://console.cloud.google.com/kubernetes/workload), select the `greeter-server` workload and click on "Container logs". ## Accessing the API from the robot In order to run `greeter-client` on the robot's Kubernetes cluster, we again package it as a Docker image and push it to our container registry, to which the robot also has access. Our deploy script offers a command to build, tag, and push the image to the cloud registry, like we did with the server container: ```shell ./deploy.sh push_client ``` And finally, to execute the script, SSH into robot and run: ```shell export PROJECT_ID=[YOUR_GCP_PROJECT_ID] docker pull grpc/cxx:1.12.0 # This may take several minutes, depending on WiFi connection kubectl run -ti --rm --restart=Never --image=gcr.io/$PROJECT_ID/greeter-client greeter-client \ -- ./greeter-client www.endpoints.$PROJECT_ID.cloud.goog:443 robot ``` You should see the server's answer `Hello robot`. Two things are noteworthy: * The `greeter-client` Docker image was pulled from the container registry without the need for additional credentials. This worked because there is a periodical job running on the robot's Kubernetes cluster that refreshes the GCR credentials. Run `kubectl get pods` on the robot and you will see pod names that start with `gcr-credential-refresher`. * `grpc::GoogleDefaultCredentials()` in the client's code automatically obtained credentials that allowed the robot to access the cloud cluster. This worked because the the local Metadata Server obtains access tokens for the robot in the background. ## Cleaning up In order to stop the service in the cloud cluster and revert the configuration changes, run: ```shell ./deploy.sh delete ``` ================================================ FILE: docs/how-to/deploying-service.md ================================================ # Deploying a service to the cloud cluster Estimated time: 60 min In this guide we will write a HTTP service in Python and deploy it to our Google Kubernetes Engine (GKE) cluster in the cloud in such a way that authentication is required for access. We will show how to access the service from the workstation and how to access it from code running in the robot's Kubernetes cluster. ## Prerequisites * Completed the [Quickstart Guide](../quickstart.md), after which the GCP project is set up and `gcloud-sdk` and `kubectl` are installed and configured. * `docker` is installed and configured on the workstation ([instructions](https://docs.docker.com/install/linux/docker-ce/ubuntu/)). * `python3`, `python3-pip`, and `curl` are installed on the workstation. * For the last part of the guide: A robot that has been successfully [connected to the cloud](connecting-robot.md). Create a directory for the code examples of this guide, e.g.: ```shell mkdir hello-service cd hello-service ``` Set your GCP project ID as an environment variable: ```shell export PROJECT_ID=[YOUR_GCP_PROJECT_ID] ``` All files created in this tutorial can be found in [docs/how-to/examples/hello-service/](https://github.com/googlecloudrobotics/core/tree/master/docs/how-to/examples/hello-service). If you download the files, you have to replace the placeholders `[PROJECT_ID]` with your GCP project ID: ```shell sed -i "s/\[PROJECT_ID\]/$PROJECT_ID/g" client/client.py server/hello-server.yaml ``` ## A simple HTTP server Create a subdirectory for the server code: ```shell mkdir server cd server ``` Paste the following into a file called `server.py`: [embedmd]:# (examples/hello-service/server/server.py python) ```python from http import server import signal import sys class MyRequestHandler(server.BaseHTTPRequestHandler): def do_GET(self): print('Received a request') self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'Server says hello!\n') def main(): # Terminate process when Kubernetes sends SIGTERM. signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) server_address = ('', 8000) httpd = server.HTTPServer(server_address, MyRequestHandler) httpd.serve_forever() if __name__ == '__main__': main() ``` This Python program implements a server that listens on port 8000 for incoming HTTP GET requests. When such a request is received, it prints a line to stdout and responds to the request with a short message. You can try it out with: ```shell python server.py ``` If you see `ImportError: No module named http`, you are most likely using Python 2.x; try `python3` instead of `python`.) Then, from another terminal on the same workstation, run: ```shell curl -i http://localhost:8000 ``` You should see the headers indicating that the request was successful (`200 OK`) and the server's response message. You can also try entering `localhost:8000` in your browser's address bar. ## Dockerizing the service Next, to prepare our Python program for deployment in the cloud, we package it as a Docker image. Make sure that the docker daemon is running and that your user has the necessary privileges: ```shell docker run --rm hello-world ``` If this command fails, make sure Docker is installed according to the [installation instructions](https://docs.docker.com/install/linux/docker-ce/ubuntu/). In the same directory as `server.py`, create a `Dockerfile` with the following contents: [embedmd]:# (examples/hello-service/server/Dockerfile dockerfile) ```dockerfile FROM python:alpine WORKDIR /data COPY server.py ./ CMD [ "python", "-u", "./server.py" ] ``` (Note: the `-u` option disables line-buffering; Python's line-buffering can prevent output from appearing immediately in the Docker logs.) To build the Docker image, run: ```shell docker build -t hello-server . ``` You should now have an image tagged `hello-server` in your local registry: ```shell docker images | grep hello-server ``` It can be run locally with: ```shell docker run -ti --rm -p 8000:8000 hello-server ``` You should now be able to send requests to the server with `curl` as before. ## Uploading the Docker image to the cloud In order to be able to run the server as a container in our cloud cluster, we need to upload the Docker image to our GCP project's private [container registry](https://cloud.google.com/container-registry/docs/pushing-and-pulling). Enable the Docker credential helper: ```shell gcloud auth configure-docker ``` Tag the image and push it to the registry: ```shell docker tag hello-server gcr.io/$PROJECT_ID/hello-server docker push gcr.io/$PROJECT_ID/hello-server ``` The image should now show up in the [Container Registry](https://console.cloud.google.com/gcr). ## Deploying the service in the cloud using Kubernetes Create a file called `hello-server.yaml` with the following contents: [embedmd]:# (examples/hello-service/server/hello-server.yaml yaml) ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-server-ingress annotations: nginx.ingress.kubernetes.io/auth-url: "http://token-vendor.default.svc.cluster.local/apis/core.token-vendor/v1/token.verify?robots=true" spec: ingressClassName: nginx rules: - host: www.endpoints.[PROJECT_ID].cloud.goog http: paths: - path: /apis/hello-server pathType: Prefix backend: service: name: hello-server-service port: number: 8000 --- apiVersion: v1 kind: Service metadata: name: hello-server-service spec: ports: - name: hello-server-port port: 8000 # the selector is used to link pods to services selector: app: hello-server-app --- apiVersion: apps/v1 kind: Deployment metadata: name: hello-server spec: # all pods matching this selector belong to this deployment selector: matchLabels: app: hello-server-app template: metadata: # the other side of the link between services and pods labels: app: hello-server-app spec: containers: - name: hello-server image: gcr.io/[PROJECT_ID]/hello-server:latest ports: # must match the port of the service - containerPort: 8000 ``` This file contains the information needed by Kubernetes to run our HTTP service in our cloud cluster. In the following, we will go over it bit by bit assuming basic familiarity with the [YAML format](https://en.wikipedia.org/wiki/YAML). We define three Kubernetes resources: * The *Ingress* contains rules that tell our cluster's nginx (the HTTP server that handles all incoming traffic) which incoming requests to forward to our service. * The *Service* defines how our service is exposed within the cluster. * The *Deployment* describes the Docker container to run. Metadata, labels, and selectors are used to tie the three resources together. A detailed explanation of the Kubernetes resources is out of scope for this guide, check out the [Kubernetes docs](https://kubernetes.io/docs/home/) to get started. There are a few points worth mentioning, though: * In the Ingress and Deployment, don't forget to replace `[PROJECT_ID]` with your GCP project ID. * In the Ingress, there is an annotation with key `nginx.ingress.kubernetes.io/auth-url`. This tells our cluster's nginx to check the authorization of each request before forwarding it to the `hello-server`. The value `http://token-vendor...` is the cluster-internal DNS address of a token verifier service that is running in the cluster as part of the Cloud Robotics Core platform. * The Ingress rules specify that our hello-server will be reachable at `https://www.endpoints.[PROJECT_ID].cloud.goog/apis/hello-server`. * The Deployment contains the full reference of the Docker image that we pushed to the Container Registry in the previous section. Make sure that `kubectl` points to the correct GCP project: ```shell kubectl config get-contexts ``` If the correct cluster is not marked with an asterisk in the output, you can switch to it with `kubectl config use-context [...]`.) Then deploy by applying the configuration: ```shell kubectl apply -f hello-server.yaml ``` You can explore the various resources that were created on your cluster as a result of this command in the [GKE Console](https://console.cloud.google.com/kubernetes/workload) or with `kubectl`, e.g.: ```shell kubectl get pods ``` The resulting list should contain a running pod with a name like `hello-server-xxxxxxxxxx-xxxxx`. ## Redeploying after a change If you make a change to `server.py`, you need to rebuild and push the Docker image: ```shell docker build -t hello-server . docker tag hello-server gcr.io/$PROJECT_ID/hello-server docker push gcr.io/$PROJECT_ID/hello-server ``` The easiest way to get Kubernetes to restart the workload with the latest version of the container is to delete the pod: ```shell kubectl delete pod -l 'app=hello-server-app' ``` Kubernetes will automatically pull the newest image and recreate the pod. If you make a change to `hello-server.yaml`, all you have to do is apply it again: ```shell kubectl apply -f hello-server.yaml ``` ## Accessing the API Let's try to access our server as we did before: ```shell curl -i https://www.endpoints.$PROJECT_ID.cloud.goog/apis/hello-server ``` This should result in a `401 Unauthorized` error because we did not supply any authorization information with the request. (Note: If you comment out the `auth-url` annotation in the Ingress definition and reapply it, this request will succeed.) We can, however, easily obtain credentials from `gcloud` and attach them to our request by means of an "Authorization" header: ```shell token=$(gcloud auth application-default print-access-token) curl -i -H "Authorization: Bearer $token" https://www.endpoints.$PROJECT_ID.cloud.goog/apis/hello-server ``` If this command fails because "Application Default Credentials are not available", you need to first run: ```shell gcloud auth application-default login --project=$PROJECT_ID ``` And follow the instructions in your browser. Recall that when running `server.py` on your workstation you were able to see the server's log output upon receiving a request. This log output is also recorded when the server is running in the cloud cluster. To inspect it, run: ```shell kubectl logs -l 'app=hello-server-app' ``` Or go to the [GKE Console](https://console.cloud.google.com/kubernetes/workload), select the `hello-server` workload and click on "Container logs". Next, let's access the API from some Python code. Eventually, we will build another Docker image from this code, so it needs to live in a separate directory: ```shell cd .. mkdir client cd client ``` Get some dependencies: ```shell pip3 install --upgrade google-auth requests ``` (Depending on your local installation, you might have to use `pip3`.) Create `client.py` with the following contents: [embedmd]:# (examples/hello-service/client/client.py python) ```python import google.auth import google.auth.transport.requests as requests credentials, project_id = google.auth.default() authed_session = requests.AuthorizedSession(credentials) response = authed_session.request( "GET", "https://www.endpoints.[PROJECT_ID].cloud.goog/apis/hello-server") print(response.status_code, response.reason, response.text) ``` Replace `[PROJECT_ID]` with your GCP project ID. This script: * uses [`google-auth`](https://google-auth.readthedocs.io/en/latest/user-guide.html) to obtain application default credentials (just as we previously did with the `gcloud` CLI), * uses the [`requests`](http://docs.python-requests.org/en/stable/) library to perform an authenticated request to our API, * prints the response to stdout. Try it out: ```shell python3 client.py ``` You will get a warning about using end user credentials. You can safely ignore this warning; we will eventually be using a robot's credentials.) ## Accessing the API from the robot In order to run this script on the robot's Kubernetes cluster, we again package it as a Docker image and push it to our container registry, to which the robot also has access. Create a `Dockerfile` containing: [embedmd]:# (examples/hello-service/client/Dockerfile dockerfile) ```dockerfile FROM python:alpine RUN pip install --no-cache-dir google-auth requests WORKDIR /data COPY client.py ./ CMD [ "python", "-u", "./client.py" ] ``` Build, tag, and push the image: ```shell docker build -t hello-client . docker tag hello-client gcr.io/$PROJECT_ID/hello-client docker push gcr.io/$PROJECT_ID/hello-client ``` And finally, to execute the script, SSH into robot and run: ```shell kubectl run -ti --rm --restart=Never --image=gcr.io/$PROJECT_ID/hello-client hello-client ``` You should see the server's message. Two things are noteworthy: * The `hello-client` Docker image was pulled from the Container Registry without the need for additional credentials. This worked because there is a periodical job running on the robot's Kubernetes cluster that refreshes the GCR credentials. Run `kubectl get pods` on the robot and you will see pod names that start with `gcr-credential-refresher`. * The `google.auth.default()` invocation in the Python code automatically obtained credentials that allowed the robot to access the cloud cluster. This worked because the `google-auth` library queried the local Metadata Server, which obtains access tokens for the robot in the background. ## Cleaning up In order to stop the service in the cloud cluster and revert the configuration changes, change to the `server` directory and run: ```shell kubectl delete -f hello-server.yaml ``` ================================================ FILE: docs/how-to/examples/charge-service/Dockerfile ================================================ FROM python:alpine WORKDIR /data COPY server.py ./ CMD [ "python", "-u", "./server.py" ] ================================================ FILE: docs/how-to/examples/charge-service/charge-action.yaml ================================================ apiVersion: example.com/v1 kind: ChargeAction metadata: name: my-charge-action ================================================ FILE: docs/how-to/examples/charge-service/charge-controller.yaml ================================================ apiVersion: metacontroller.k8s.io/v1alpha1 kind: CompositeController metadata: name: charge-controller spec: generateSelector: true parentResource: apiVersion: example.com/v1 resource: chargeactions resyncPeriodSeconds: 1 hooks: sync: webhook: url: http://charge-controller.default:8000/sync --- apiVersion: v1 kind: Service metadata: name: charge-controller spec: selector: app: charge-controller ports: - port: 8000 --- apiVersion: apps/v1 kind: Deployment metadata: name: charge-controller spec: replicas: 1 selector: matchLabels: app: charge-controller template: metadata: labels: app: charge-controller spec: containers: - name: controller image: gcr.io/[PROJECT_ID]/charge-controller ports: - containerPort: 8000 ================================================ FILE: docs/how-to/examples/charge-service/charge-crd.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: chargeactions.example.com annotations: cr-syncer.cloudrobotics.com/spec-source: cloud spec: group: example.com names: kind: ChargeAction plural: chargeactions singular: chargeaction scope: Namespaced versions: - name: v1 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object x-kubernetes-preserve-unknown-fields: true --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:cr-syncer:chartaction labels: cr-syncer.cloudrobotics.com/aggregate-to-robot-service: "true" rules: - apiGroups: - example.com resources: - chargeactions verbs: - get - list - watch - update ================================================ FILE: docs/how-to/examples/charge-service/server.py ================================================ from http.server import BaseHTTPRequestHandler, HTTPServer import json import signal import sys import time import uuid class Controller(BaseHTTPRequestHandler): def sync(self, parent, children): """Actuate a ChargeAction custom resource. The resource is actuated by using the Charge Service to send the robot to a charger. While charging is in progress, the status of the resource is updated to reflect the charge level. Because the ChargeAction is handled by an external service, this controller doesn't create any child resources, ie the `children` list is empty. Args: parent: The current ChargeAction resource. children: Unused. Returns: A dict containing the latest status of the action, and an empty list of children, eg: { "status": { "state": "OK", }, "children": [], } """ # Get current status and copy to start building next status. current_status = parent.get("status", None) or {} desired_status = dict(current_status) state = current_status.get("state", "CREATED") if state == "CREATED": # The ChargeAction has just been created. Use the external Charge Service # to start charging. Store the request ID in the status so we can use it # to check the state of the charge request. request_id = self.charge_service.start_charging() desired_status["state"] = "IN_PROGRESS" desired_status["request_id"] = request_id elif state == "IN_PROGRESS": try: # Get the progress of the charge request from the external service. progress = self.charge_service.get_progress( current_status["request_id"]) desired_status["charge_level_percent"] = progress if progress == 100: # Charging has completed. desired_status["state"] = "OK" except ValueError as e: # The charge request was not found. This could be because the robot was # restarted during a charge, and the request was forgotten. desired_status["state"] = "ERROR" desired_status["message"] = str(e) elif state in ["OK", "CANCELLED", "ERROR"]: # Terminal state, do nothing. pass else: desired_status["state"] = "ERROR" desired_status["message"] = "Unrecognized state: %r" % state return {"status": desired_status, "children": []} def do_POST(self): """Serve the sync() function as a JSON webhook.""" observed = json.loads(self.rfile.read(int(self.headers["content-length"]))) desired = self.sync(observed["parent"], observed["children"]) self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() self.wfile.write(json.dumps(desired).encode('utf-8')) class ChargeService(object): """ChargeService wraps an external API that send the robot to a charger. For this example, it just fakes the charging process. """ SECONDS_FOR_FULL_CHARGE = 10 def __init__(self): self._requests = {} def start_charging(self): request_id = str(uuid.uuid4()) self._requests[request_id] = time.time() return request_id def get_progress(self, request_id): if request_id not in self._requests: raise ValueError("invalid request ID") charge_time = time.time() - self._requests[request_id] if charge_time > self.SECONDS_FOR_FULL_CHARGE: return 100 else: return int(100 * (charge_time / self.SECONDS_FOR_FULL_CHARGE)) # Terminate process when Kubernetes sends SIGTERM. signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) Controller.charge_service = ChargeService() HTTPServer(("", 8000), Controller).serve_forever() ================================================ FILE: docs/how-to/examples/greeter-service/Makefile ================================================ # # Copyright 2019 The Cloud Robotics Authors # # 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. # HOST_SYSTEM = $(shell uname | cut -f 1 -d_) SYSTEM ?= $(HOST_SYSTEM) CXX = g++ CPPFLAGS += `pkg-config --cflags protobuf grpc` CXXFLAGS += -std=c++11 -I . ifeq ($(SYSTEM),Darwin) LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++ grpc`\ -lgrpc++_reflection\ -ldl else LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++ grpc`\ -Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed\ -ldl endif PROTOC = protoc GRPC_CPP_PLUGIN = grpc_cpp_plugin GRPC_CPP_PLUGIN_PATH ?= `which $(GRPC_CPP_PLUGIN)` PROTOS_PATH = ./proto/ vpath %.proto $(PROTOS_PATH) all: greeter-server greeter-client greeter-server: helloworld.pb.o helloworld.grpc.pb.o server/server.o $(CXX) $^ $(LDFLAGS) -o $@ greeter-client: helloworld.pb.o helloworld.grpc.pb.o client/client.o $(CXX) $^ $(LDFLAGS) -o $@ .PRECIOUS: %.grpc.pb.cc %.grpc.pb.cc: %.proto $(PROTOC) -I $(PROTOS_PATH) --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $< .PRECIOUS: %.pb.cc %.pb.cc: %.proto $(PROTOC) -I $(PROTOS_PATH) --cpp_out=. $< clean: rm -f *.o client/*.o server/*.o *.pb *.pb.cc *.pb.h ================================================ FILE: docs/how-to/examples/greeter-service/client/Dockerfile ================================================ FROM grpc/cxx:1.12.0 WORKDIR /data COPY client/client.cc ./client/ COPY proto/helloworld.proto ./proto/ COPY Makefile ./ RUN make greeter-client && make clean CMD ["./greeter-client"] ================================================ FILE: docs/how-to/examples/greeter-service/client/client.cc ================================================ /* * * Copyright 2019 The Cloud Robotics Authors * * 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. * */ #include #include #include #include #include "helloworld.grpc.pb.h" using grpc::Channel; using grpc::ChannelCredentials; using grpc::ClientContext; using grpc::Status; using helloworld::Greeter; using helloworld::HelloReply; using helloworld::HelloRequest; class GreeterClient { public: GreeterClient(std::shared_ptr channel) : stub_(Greeter::NewStub(channel)) {} // Assembles the client's payload, sends it and presents the response back // from the server. std::string SayHello(const std::string& user) { // Data we are sending to the server. HelloRequest request; request.set_name(user); // Container for the data we expect from the server. HelloReply reply; // Context for the client. It could be used to convey extra information to // the server and/or tweak certain RPC behaviors. ClientContext context; // The actual RPC. Status status = stub_->SayHello(&context, request, &reply); // Act upon its status. if (status.ok()) { return reply.message(); } else { std::cout << status.error_code() << ": " << status.error_message() << std::endl; return "RPC failed"; } } private: std::unique_ptr stub_; }; int main(int argc, char** argv) { if (argc < 2) { const std::string client_path(argv[0]); std::cout << "Usage:" << std::endl; std::cout << " " << client_path << " []" << std::endl; std::cout << "Example:" << std::endl; std::cout << " " << client_path << " www.endpoints.${PROJECT_ID}.cloud.goog:443" << std::endl; return 0; } // The first parameter is the server's address, optionally containing the // port. std::string grpc_endpoint(argv[1]); if (grpc_endpoint.find(":") == std::string::npos) { // Set the default port of the server. grpc_endpoint += ":50051"; } // The optional second parameter is the name to be sent to the server. std::string name("world"); if (argc >= 3) { name = argv[2]; } std::cout << "Sending request to " << grpc_endpoint << " ..." << std::endl; // We are communicating via SSL to the endpoint service using the credentials // of the user or robot running the client. // We don't use credentials when connecting to localhost for testing. std::shared_ptr channel_creds; if (grpc_endpoint.find("localhost:") == 0 || grpc_endpoint.find("127.0.0.1:") == 0) { channel_creds = grpc::InsecureChannelCredentials(); } else { channel_creds = grpc::GoogleDefaultCredentials(); } GreeterClient greeter(grpc::CreateChannel(grpc_endpoint, channel_creds)); std::string user(name); std::string reply = greeter.SayHello(user); std::cout << "Greeter received: " << reply << std::endl; return 0; } ================================================ FILE: docs/how-to/examples/greeter-service/deploy.sh ================================================ #!/bin/bash # # Copyright 2019 The Cloud Robotics Authors # # 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. set -o pipefail -o errexit DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd ${DIR} function die { echo "$1" >&2 exit 1 } function push_image { local target=$1 docker build -f "${target}/Dockerfile" -t "greeter-${target}" . docker tag "greeter-${target}" "gcr.io/${PROJECT_ID}/greeter-${target}" docker push "gcr.io/${PROJECT_ID}/greeter-${target}" } function create_config { cat greeter-server.yaml.tmpl | envsubst >greeter-server.yaml } # public functions function push_client { push_image client } function update_config { create_config kubectl apply -f greeter-server.yaml } function update_server { push_image server kubectl delete pod -l 'app=greeter-server-app' update_config } function create { push_image server push_client update_config } function delete { create_config kubectl delete -f greeter-server.yaml } # main if [[ -z ${PROJECT_ID} ]]; then die "Set PROJECT_ID first: export PROJECT_ID=[GCP project id]" fi if [[ ! "$1" =~ ^(create|delete|update_config|update_server|push_client)$ ]]; then die "Usage: $0 {create|delete|update_config|update_server|push_client}" fi # call arguments verbatim: "$@" ================================================ FILE: docs/how-to/examples/greeter-service/greeter-server.yaml.tmpl ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: greeter-server-ingress annotations: nginx.ingress.kubernetes.io/backend-protocol: GRPC nginx.ingress.kubernetes.io/auth-url: "http://token-vendor.default.svc.cluster.local/apis/core.token-vendor/v1/token.verify?robots=true" spec: ingressClassName: nginx rules: - host: "www.endpoints.${PROJECT_ID}.cloud.goog" http: paths: # must match the namespace and service name in the proto - path: /helloworld.Greeter/ pathType: Prefix backend: service: name: greeter-server-service # must match the port used in server.cc port: number: 50051 --- apiVersion: v1 kind: Service metadata: name: greeter-server-service spec: ports: - # optional descriptive name for the service port name: grpc-port # must match the service port specified in ingress port: 50051 # the selector is used to link pods to services selector: app: greeter-server-app --- apiVersion: apps/v1 kind: Deployment metadata: name: greeter-server spec: replicas: 1 # all pods matching this selector belong to this deployment selector: matchLabels: app: greeter-server-app template: metadata: # the other side of the link between services and pods labels: app: greeter-server-app spec: containers: - name: greeter-server image: "gcr.io/${PROJECT_ID}/greeter-server:latest" ports: # must match the port of the service - containerPort: 50051 ================================================ FILE: docs/how-to/examples/greeter-service/proto/helloworld.proto ================================================ // Copyright 2019 The Cloud Robotics Authors // // 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. syntax = "proto3"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting. rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings. message HelloReply { string message = 1; } ================================================ FILE: docs/how-to/examples/greeter-service/server/Dockerfile ================================================ FROM grpc/cxx:1.12.0 WORKDIR /data COPY server/server.cc ./server/ COPY proto/helloworld.proto ./proto/ COPY Makefile ./ RUN make greeter-server && make clean CMD ["./greeter-server"] ================================================ FILE: docs/how-to/examples/greeter-service/server/server.cc ================================================ /* * * Copyright 2019 The Cloud Robotics Authors * * 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. * */ #include #include #include #include #include #include #include #include "helloworld.grpc.pb.h" using grpc::Server; using grpc::ServerBuilder; using grpc::ServerContext; using grpc::Status; using helloworld::Greeter; using helloworld::HelloReply; using helloworld::HelloRequest; // The gRPC server is defined globally so that SIGTERM handler can shut it // down when Kubernetes stops the process. std::unique_ptr server; // Logic and data behind the server's behavior. class GreeterServiceImpl final : public Greeter::Service { Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override { std::cout << "Received request: " << request->ShortDebugString() << std::endl; std::string prefix("Hello "); reply->set_message(prefix + request->name()); return Status::OK; } }; void RunServer() { std::string server_address("0.0.0.0:50051"); GreeterServiceImpl service; ServerBuilder builder; // Listen on the given address without any authentication mechanism. Cloud // Robotics Core ensures that clients are authenticated. builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); // Register "service" as the instance through which we'll communicate with // clients. In this case it corresponds to a *synchronous* service. builder.RegisterService(&service); // Finally assemble the server. server = builder.BuildAndStart(); std::cout << "Server listening on " << server_address << std::endl; std::signal(SIGTERM, [](int) { // When SIGTERM is received, shutdown the gRPC server. server->Shutdown(); }); // Wait for the server to shutdown. server->Wait(); } int main(int argc, char** argv) { RunServer(); return 0; } ================================================ FILE: docs/how-to/examples/hello-service/client/Dockerfile ================================================ FROM python:alpine RUN pip install --no-cache-dir google-auth requests WORKDIR /data COPY client.py ./ CMD [ "python", "-u", "./client.py" ] ================================================ FILE: docs/how-to/examples/hello-service/client/client.py ================================================ import google.auth import google.auth.transport.requests as requests credentials, project_id = google.auth.default() authed_session = requests.AuthorizedSession(credentials) response = authed_session.request( "GET", "https://www.endpoints.[PROJECT_ID].cloud.goog/apis/hello-server") print(response.status_code, response.reason, response.text) ================================================ FILE: docs/how-to/examples/hello-service/server/Dockerfile ================================================ FROM python:alpine WORKDIR /data COPY server.py ./ CMD [ "python", "-u", "./server.py" ] ================================================ FILE: docs/how-to/examples/hello-service/server/hello-server.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-server-ingress annotations: nginx.ingress.kubernetes.io/auth-url: "http://token-vendor.default.svc.cluster.local/apis/core.token-vendor/v1/token.verify?robots=true" spec: ingressClassName: nginx rules: - host: www.endpoints.[PROJECT_ID].cloud.goog http: paths: - path: /apis/hello-server pathType: Prefix backend: service: name: hello-server-service port: number: 8000 --- apiVersion: v1 kind: Service metadata: name: hello-server-service spec: ports: - name: hello-server-port port: 8000 # the selector is used to link pods to services selector: app: hello-server-app --- apiVersion: apps/v1 kind: Deployment metadata: name: hello-server spec: # all pods matching this selector belong to this deployment selector: matchLabels: app: hello-server-app template: metadata: # the other side of the link between services and pods labels: app: hello-server-app spec: containers: - name: hello-server image: gcr.io/[PROJECT_ID]/hello-server:latest ports: # must match the port of the service - containerPort: 8000 ================================================ FILE: docs/how-to/examples/hello-service/server/server.py ================================================ from http import server import signal import sys class MyRequestHandler(server.BaseHTTPRequestHandler): def do_GET(self): print('Received a request') self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'Server says hello!\n') def main(): # Terminate process when Kubernetes sends SIGTERM. signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) server_address = ('', 8000) httpd = server.HTTPServer(server_address, MyRequestHandler) httpd.serve_forever() if __name__ == '__main__': main() ================================================ FILE: docs/how-to/running-ros-node.md ================================================ # Running a ROS node as a Kubernetes deployment Estimated time: 10 min The following instructions describe how to setup a Kubernetes cluster on a robot running Ubuntu 20.04 and run a ROS node on it. The installation script installs and configures: * Docker * A single-node Kubernetes cluster (packages: kubectl, kubeadm, kubelet) Once you've done this, you can use Kubernetes to: * Reduce downtime during updates with Kubernetes deployments * Apply CPU, disk or memory quotas to individual processes * Add additional compute nodes to the cluster, such as an Nvidia Jetson * Use a network plugin to apply network access control * Manage project configuration or sensitive secrets such as account credentials For more details, refer to the [Kubernetes documentation](https://kubernetes.io/docs/home/). ## Installing the cluster on the robot See . ## Run a ROS node with Kubernetes If you're already using ROS on your robot, you can run a ROS node inside Kubernetes that will communicate with other nodes on the robot. If not, you can follow the [ROS tutorials](http://wiki.ros.org/ROS/Tutorials) to get started. First, make sure you're running `roscore`. In another terminal, please run: ```shell roscore ``` > **Caution:** If you have a more complicated ROS setup, such as a ROS master running on another machine, you might need to change `ROS_MASTER_URI` or `ROS_IP` in rostopic-echo.yaml. You can run a ROS node by creating a Kubernetes Deployment object, and you can describe a Deployment in a YAML file. For example, this YAML file describes a Deployment that runs `rostopic echo`. Create file called `rostopic-echo.yaml` with the following contents: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: rostopic-echo spec: selector: matchLabels: app: rostopic-echo template: metadata: labels: app: rostopic-echo spec: containers: - name: rostopic-echo image: ros:melodic-ros-core args: - rostopic - echo - chatter env: - name: ROS_MASTER_URI value: http://192.168.9.1:11311 - name: ROS_IP value: 192.168.9.1 hostNetwork: true ``` > **Note:** For simplicity, this example uses `hostNetwork: true` to disable network isolation. Advanced users can disable host networking to improve security. For more information, see the networking documentation for Docker, Kubernetes and ROS. After creating `rostopic-echo.yaml`, use `kubectl` to apply it to your cluster: ```shell kubectl apply -f rostopic-echo.yaml ``` Depending on your internet connection, it will take a minute or so to download the Docker image. Wait until you see `Running`: ```console $ watch kubectl get pods -l app=rostopic-echo NAMESPACE NAME READY STATUS RESTARTS AGE default rostopic-echo-576cbf47c7-dtlc6 1/1 Running 0 1m ``` Now, publish a ROS message and check that it was received inside Kubernetes: ```console $ rostopic pub -1 chatter std_msgs/String "Hello, world" $ kubectl logs -l app=rostopic-echo data: "Hello, world" --- ``` Kubernetes will keep this node running until you delete the deployment: ```shell kubectl delete -f rostopic-echo.yaml ``` ================================================ FILE: docs/how-to/setting-up-oauth.md ================================================ # Setting up OAuth for web UIs Estimated time: 5 min When a user loads a web UI hosted in the cloud Kubernetes cluster, the server has to authenticate them before allowing them to use the service. To enable this, you'll need to set up OAuth with the Cloud Console. Once you've completed these steps, you'll be able to access services with web UIs, such as [Grafana](https://grafana.com/). If you haven't already, complete the [Quickstart Guide](../quickstart.md) or [Deploy Cloud Robotics Core from sources](deploy-from-sources.md) to set up your GCP project. ## Create OAuth credentials 1. Open the [cloud console](https://console.cloud.google.com/) and ensure that your cloud project is selected in the project selector dropdown at the top. 1. Configure the OAuth consent screen: [APIs & Services → Credentials → OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent). * User Type: Internal * Application name: My Cloud Robotics Application * Support email: *your email address* * Add `[PROJECT_ID].cloud.goog` to Authorized domains (where `[PROJECT_ID]` is your GCP project ID). * Leave the other fields blank. 1. Create an OAuth client ID: [APIs & Services → Credentials → Create credentials → OAuth client ID](https://console.cloud.google.com/apis/credentials/oauthclient). * Application type: Web application * Restrictions → Authorized JavaScript origins:
`https://www.endpoints.[PROJECT_ID].cloud.goog` * Restrictions → Authorized redirect URIs:
`https://www.endpoints.[PROJECT_ID].cloud.goog/oauth2/callback` * Click "Create". You'll see a dialog containing the client ID and secret which we will add to your `config.sh` next. ## Update your config and redeploy 1. update your `config.sh` in the Google Cloud Storage bucket: ```shell curl -fS "https://storage.googleapis.com/cloud-robotics-releases/run-install.sh" >run-install.sh bash ./run-install.sh $PROJECT_ID --set-oauth ``` Enter the OAuth client ID and secret from the previous step when asked. 1. Update your cloud project: ```shell bash ./run-install.sh $PROJECT_ID ``` After the update has been deployed, OAuth is enabled in your cloud project. Verify that `oauth2-proxy` is running now: ```console $ kubectl get pods NAME READY STATUS RESTARTS AGE ... oauth2-proxy-xxx 1/1 Running 0 1m ``` ## Try it out Open a web browser and visit `https://www.endpoints.[PROJECT_ID].cloud.goog/grafana/dashboards`, replacing `[PROJECT_ID]` with your GCP project ID. You'll be prompted to log in with your Google account, after which you'll see a list of dashboards. Try selecting "Kubernetes Capacity Planning" to see the resource usage of the Kubernetes cluster. ================================================ FILE: docs/how-to/using-cloud-storage.md ================================================ # Using Cloud Storage from a robot Estimated time: 20 minutes This page describes a simple Cloud Storage transaction that demonstrates how Google Cloud APIs can be accessed without additional authentication configuration from within the robot's Kubernetes cluster. Normally, to access a private Cloud Storage bucket from a robot, you'd need to manage a service account for the robot through Identity & Access Management (IAM). Cloud Robotics handles the robot's identity for you, so you can connect securely without additional configuration. 1. If you haven't already, complete the [Connecting a robot to the cloud](connecting-robot.md) steps. 1. Choose a name for the Cloud Storage bucket. In the course of this guide, the robot will upload a file into a private bucket. The bucket namespace is global, so we must take care to choose a bucket name that is not in use yet by any other user of GCP. See also the [bucket naming requirements](https://cloud.google.com/storage/docs/naming), and [best practices](https://cloud.google.com/storage/docs/best-practices#naming). For this guide we will assume a bucket name like `robot-hello-world-dc1bb474`, where the part after the last dash is a random hexadecimal number. You can generate your own unique bucket name with the command ```shell echo robot-hello-world-$(tr -dc 'a-f0-9' < /dev/urandom | head -c8) ``` Note: If the bucket name is already in use, creating the bucket in the next step will fail. In this case, choose a different bucket name. 1. Create the Cloud Storage bucket. On your workstation, run: ```shell gcloud storage buckets create gs://[BUCKET_NAME] ``` Replace `[BUCKET_NAME]` with the name of the bucket you created, e.g., `robot-hello-world-dc1bb474`. `gcloud storage` contains the sub-commands for accessing Cloud Storage, it is part of the `gcloud-sdk` package. Note that the bucket is not publicly writable, as can be verified in the [Cloud Storage browser](https://console.cloud.google.com/storage/browser). 1. Drop a file into the bucket from the robot. On the robot, run: ```console docker pull python:alpine kubectl run python --restart=Never --rm -ti --image=python:alpine -- /bin/sh # apk add gcc musl-dev libffi-dev # pip3 install google-cloud-storage # python3 >>> from google.cloud import storage >>> client = storage.Client() >>> bucket = client.bucket("[BUCKET_NAME]") >>> bucket.blob("hello_world.txt").upload_from_string("Hello, I am a robot!\n") ``` Replace `[BUCKET_NAME]` with the name of the bucket you created. 1. Verify that the file was uploaded. On your workstation, run: ```shell gcloud storage cat gs://[BUCKET_NAME]/hello_world.txt ``` This should result in the output `Hello, I am a robot!`. So why was the robot able to drop a file in the non-public bucket? There is a lot going on in the background that enabled the configuration-less secure API access: * When the robot was connected to the cloud, it generated a new private key and registered the corresponding public key in a device registry, e.g., as Kubernetes configmaps. * The setup-robot command also started a Metadata Server as a workload in the robot's Kubernetes cluster. You can verify it is running with `kubectl get pods`. The Metadata Server identifies itself to the cloud using the robot's private key and obtains short-lived access tokens in the background. * Every time a client library performs a call to a Google Cloud API, it asks the local Metadata Server for an access token. * The permissions of the robot can be inspected and managed in the Cloud Console under "IAM & admin"; you will notice that there is a service account called `robot-service@[PROJECT_ID].iam.gserviceaccount.com`, which has "Storage Admin" permissions. These permissions allowed the robot to write to the private bucket. What's next: * You can experiment with accessing other Google Cloud APIs, such as [Logging](https://cloud.google.com/logging/docs/) or [Pub/Sub](https://cloud.google.com/pubsub/docs/), from the robot programmatically. Also, Python is not the only programming language with Google Cloud client libraries: the APIs can be accessed, e.g., from code written in [Go](https://cloud.google.com/storage/docs/reference/libraries#client-libraries-install-go) in a similar configuration-less manner. * [Write your own service](deploying-service.md) that runs as a container in the cloud and provides an API that can be accessed securely from the robot. ================================================ FILE: docs/index.md ================================================ Google's Cloud Robotics Core is an open source platform that provides infrastructure essential to building and running robotics solutions for business automation. Cloud Robotics Core makes managing robot fleets easy for developers, integrators, and operators. It enables: * packaging and distribution of applications * secure, bidirectional robot-cloud communication * easy access to Google Cloud services such as ML, logging, and monitoring. ![Cloud Robotics Core overview](cloud-robotics-core-overview.png) ### Documentation * [Quickstart](quickstart.md): Set up Cloud Robotics from binaries. * [Overview](overview.md): Develop a deeper understanding of Cloud Robotics. * Concepts * Common: [Project configuration](concepts/config.md) * Layer 1: [Federation](concepts/federation.md), [Device Identity](concepts/device_identity.md) * Layer 2: [Application Management](concepts/app-management.md) * How-to guides * [Deploying Cloud Robotics from sources](how-to/deploy-from-sources)
Build and deploy Cloud Robotics from the sources hosted on Github using Bazel. * [Running a ROS node as a Kubernetes deployment](how-to/running-ros-node.md)
Use Kubernetes to administer containerized workloads on a robot. * [Setting up OAuth for web UIs](how-to/setting-up-oauth.md)
Use services like Grafana with a web browser. * [Connecting a robot to the cloud](how-to/connecting-robot.md)
Enable secure communication between a robot and the Google Cloud Platform. * [Using Cloud Storage from a robot](how-to/using-cloud-storage.md)
Programmatically store data from the robot with Cloud Storage. * [Deploying a service to the cloud](how-to/deploying-service.md)
Run an API service in the cloud cluster and access it from a robot. * [Deploying a gRPC service](how-to/deploying-grpc-service.md)
Run an gRPC service written in C++ in the cloud cluster and access it from a robot. * [Creating a declarative API](how-to/creating-declarative-api.md)
Create a Kubernetes-style declarative API and run it on the cloud or on a robot. * Development * [Debugging authentication problems](developers/debug-auth.md)
Useful tips for working with Authentication and Authorization systems. ================================================ FILE: docs/overview.md ================================================ # Overview of Cloud Robotics Core To understand Cloud Robotics Core, you should be familiar with the following concepts: **Docker containers** : Containers decouple applications from the environment in which they run. They let you deploy applications easily and consistently, regardless of whether the target environment is a robot, an on-premise data center, or the public cloud. Docker is a popular, open-source container format. Read more about [Containers at Google](https://cloud.google.com/containers/). **Kubernetes** : Kubernetes is an open-source system to deploy, scale, and manage containerized applications anywhere. It lets you deploy containerized applications onto your robots, run them on one or more compute nodes and manage associated resources like configuration settings or networking. Read more in the [Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/). **Helm** : Helm is a tool for managing Kubernetes charts. Charts are packages of pre-configured Kubernetes resources. Read more in the [Helm documentation](https://github.com/helm/helm/blob/master/README.md). **Custom Resource (CR)** : Custom resources are extensions of the Kubernetes API that let you manage application-specific data, offering [many features](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#common-features). To use a custom resource, you first have to create a Custom Resource Definition (CRD). Read more about [extending the Kubernetes API with Custom Resource Definitions](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/). ## Layers Cloud Robotics Core builds upon standard Kubernetes management tools and several open-source packages. We structure Cloud Robotics Core into several layers that address distinct needs. ![Cloud Robotics Core layers](cloud-robotics-core-layers.png) ### Layer 0: Kubernetes on Robot Layer 0 is an on-robot Kubernetes setup optimized for single-node clusters. It lets you deploy containerized workloads onto the robot, run them on one or more compute nodes and manage associated resources like configuration settings or networking without the overhead of a VM. You can try it out by following the [Running a ROS node as a Kubernetes deployment](how-to/running-ros-node) How-to Guide. ### Layer 1: Robot Fleet Connectivity and Security Layer 1 provides secure communication and access control. Every robot is identified by a unique keypair. The public key is managed in as Kubernetes configmap. A cloud-based authorization service uses these keys to authenticate robots and generate short-lived OAuth access tokens. This approach follows the [BeyondCorp](https://cloud.google.com/beyondcorp/) zero trust network model: all connections are authenticated and authorized individually, without a need for a traditional VPN. A specific robot's access may be revoked if needed. The same set of credentials also serves as foundation for Apps to securely communicate with the cloud over gRPC. You can try it out by following the [Connecting a robot to the cloud](how-to/connecting-robot) How-to Guide. Layer 1 also provides a light-weight cluster federation system that synchronizes selected custom resources across the fleet. This provides developers with a pattern for command & control that is robust against intermittent connectivity and fits well with the overall declarative Kubernetes model. You can read more in the concept guide about [Federation](concepts/federation.md). > **Note:** Layer 2 and 3 (below) are currently under development. You'll find an early > implementation of these layers in our repository but corresponding APIs and concepts are not stable yet. ### Layer 2: App Management Layer 2 introduces App management, built as a lightweight facade on top of Kubernetes and the Helm package manager. In Cloud Robotics Core, Apps consist of one or more Docker containers and associated resources, that run on the robot and in the cloud. The App management layer determines which Apps, and app components, run on robots and in the cloud. The concept guide has more details on [Application Management](concepts/app-management.md). ### Layer 3: Managed Repositories Layer 3 adds the capability to download Apps from remote repositories. Most of our core platform services will be packaged as optional downloadable extensions via a Google-managed repository. In addition, vendor-managed repositories can be added and used for locating new or updated vendor- provided Apps. Apps provide DevOps and robotics services such as: * Logs aggregation and stack traces with StackDriver * Metric collection, upload and dashboarding with Prometheus and Grafana * Sensor data transport for cloud-based analysis * Remote debugging using RViz over WebRTC * Remote administration ================================================ FILE: docs/quickstart.md ================================================ # Quickstart Estimated time: 10 min This page describes how to set up a Google Cloud Platform (GCP) project containing the Cloud Robotics Core (CRC) components. In particular, this creates a cluster with Google Kubernetes Engine and prepares it to accept connections from robots, which enables those robots to securely communicate with GCP. The commands were tested on machines running Debian (Stretch) or Ubuntu (16.04 and 18.04) Linux. 1. In the GCP Console, go to the [Manage resources][resource-manager] page and select or create a project. 1. Make sure that [billing][modify-project] is enabled for your project. 1. [Install the Cloud SDK][cloud-sdk]. When prompted, choose the project you created above. 1. After installing the Cloud SDK, install the `kubectl` command-line tool: ```shell gcloud components install kubectl gke-gcloud-auth-plugin ``` If you're using Cloud Shell, Debian, or Ubuntu, you may need to use apt instead: ```shell apt-get install kubectl google-cloud-sdk-gke-gcloud-auth-plugin ``` 1. Install tools required for installation: ```shell sudo apt-get install curl tar xz-utils ``` ## Deploy the project 1. Create application default credentials, which are used to deploy the cloud project. ```shell gcloud auth application-default login ``` 1. Create a directory for CRC installer. ```shell mkdir cloud-robotics cd cloud-robotics ``` 1. Set your GCP project ID as an environment variable. ```shell export PROJECT_ID=[YOUR_GCP_PROJECT_ID] ``` 1. Install the latest nightly build into your GCP project by running the install script. Accept the default configuration by hitting `ENTER` on all questions; you can change the settings later. ```shell curl -fS "https://storage.googleapis.com/cloud-robotics-releases/run-install.sh" >run-install.sh bash ./run-install.sh $PROJECT_ID ``` The install script created a Kubernetes cluster using Google Kubernetes Engine and used [Synk][synk] to install the Cloud Robotics Core component helm charts. You can browse these components on the [Workloads dashboard][workloads]. Alternatively, you can list them from the console on your workstation: ```console $ kubectl get pods NAME READY STATUS RESTARTS AGE cert-manager-xxx 1/1 Running 0 1m nginx-ingress-xxx 1/1 Running 0 1m oauth2-proxy-xxx 0/1 CrashLoopBackOff 4 1m token-vendor-xxx 1/1 Running 0 1m ``` > **Note** Unless you already set up OAuth, the `oauth2-proxy` will show an error which we will ignore for now. In addition to the cluster, the install script also created: * the [[PROJECT_ID]-cloud-robotics-config bucket][storage-bucket], containing a `config.sh` and a Terraform state which are necessary to update your cloud project later, * the [[PROJECT_ID]-robot Cloud Storage bucket][storage-bucket], containing the scripts that connect robots to the cloud, and * the [Identity & Access Management policies][iam] that authorize robots and humans to communicate with GCP. ## Update the project To update your Cloud Robotics configuration, run the install script with the `--set-config` flag. ```shell bash ./run-install.sh $PROJECT_ID --set-config ``` This command only updates the config but does not update your cloud project. To update the installation to the latest version and apply config changes, run the installer again. ```shell bash ./run-install.sh $PROJECT_ID ``` If you deleted the install scipt or you want to run an update from another machine which has the Cloud SDK installed, simply run: ``` curl -fS "https://storage.googleapis.com/cloud-robotics-releases/run-install.sh"\ | bash -s -- $PROJECT_ID ``` ## Clean up The following command will delete: * the [cloud-robotics Kubernetes cluster](https://console.cloud.google.com/kubernetes/list) This can be useful if the cluster is in a broken state. Be careful with this invocation, since you'll have to redeploy the project and reconnect any robots afterwards. ```shell curl -fS "https://storage.googleapis.com/cloud-robotics-releases/run-install.sh"\ | bash -s -- $PROJECT_ID --delete ``` > **Known issue** After deleting CRC from your project, the endpoint services will be in a "pending deletion" state for 30 days. > If you want to reinstall CRC into the same project again, you have to [undelete the services][undelete-service] manually. If you want to completely shut down the project, see [the Resource Manager documentation][shutting_down_projects]. ## Next steps * [Connect a robot to the cloud](how-to/connecting-robot.md). * [Set up OAuth](how-to/setting-up-oauth.md) [resource-manager]: https://console.cloud.google.com/cloud-resource-manager [modify-project]: https://cloud.google.com/billing/docs/how-to/modify-project [cloud-sdk]: https://cloud.google.com/sdk/docs/ [workloads]: https://console.cloud.google.com/kubernetes/workload [storage-bucket]: https://console.cloud.google.com/storage/browser [iam]: https://console.cloud.google.com/iam-admin/iam [undelete-service]: https://cloud.google.com/sdk/gcloud/reference/endpoints/services/undelete [shutting_down_projects]: https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects [synk]: https://github.com/googlecloudrobotics/core/tree/master/src/go/cmd/synk/README.md ================================================ FILE: new_versions.txt ================================================ { "cert-manager": "1.13.2", "ingress-nginx": "1.9.4", "oauth2-proxy": "7.5.1", "stackdriver-logging-agent": "1.10.1" } ================================================ FILE: non_module_deps.bzl ================================================ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # -- load statements -- # def _non_module_deps_impl(ctx): # Sysroot and libc # How to upgrade: # - Find image in https://storage.googleapis.com/chrome-linux-sysroot/ for amd64 for # a stable Linux (here: Debian bullseye), of this pick a current build. # - Verify the image contains expected /lib/x86_64-linux-gnu/libc* and defines correct # __GLIBC_MINOR__ in /usr/include/features.h # - If system files are not found, add them in ../BUILD.sysroot http_archive( name = "com_googleapis_storage_chrome_linux_amd64_sysroot", build_file = Label("//bazel:BUILD.sysroot"), sha256 = "5df5be9357b425cdd70d92d4697d07e7d55d7a923f037c22dc80a78e85842d2c", urls = [ # features.h defines GLIBC 2.31. "https://storage.googleapis.com/chrome-linux-sysroot/toolchain/4f611ec025be98214164d4bf9fbe8843f58533f7/debian_bullseye_amd64_sysroot.tar.xz", ], ) http_archive( name = "bazel_gomock", urls = [ "https://github.com/jmhodges/bazel_gomock/archive/fde78c91cf1783cc1e33ba278922ba67a6ee2a84.tar.gz", ], sha256 = "692421b0c5e04ae4bc0bfff42fb1ce8671fe68daee2b8d8ea94657bb1fcddc0a", strip_prefix = "bazel_gomock-fde78c91cf1783cc1e33ba278922ba67a6ee2a84", ) http_archive( name = "kubernetes_helm", urls = [ "https://get.helm.sh/helm-v2.17.0-linux-amd64.tar.gz", ], sha256 = "f3bec3c7c55f6a9eb9e6586b8c503f370af92fe987fcbf741f37707606d70296", strip_prefix = "linux-amd64", build_file = "//third_party/helm2:BUILD.bazel", ) http_archive( name = "kubernetes_helm3", urls = [ "https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz", ], sha256 = "1484ffb0c7a608d8069470f48b88d729e88c41a1b6602f145231e8ea7b43b50a", strip_prefix = "linux-amd64", build_file = "//third_party/helm3:BUILD.bazel", ) http_archive( name = "hashicorp_terraform", urls = [ "https://releases.hashicorp.com/terraform/1.11.4/terraform_1.11.4_linux_amd64.zip", ], sha256 = "1ce994251c00281d6845f0f268637ba50c0005657eb3cf096b92f753b42ef4dc", build_file = "//third_party:terraform.BUILD", ) http_archive( name = "com_github_kubernetes_sigs_application", urls = [ "https://github.com/kubernetes-sigs/application/archive/c8e2959e57a02b3877b394984a288f9178977d8b.tar.gz", ], sha256 = "8bafd7fb97563d1a15d9afc68c87e3aabd664f60bd8005f1ae685d79842c1ac4", strip_prefix = "application-c8e2959e57a02b3877b394984a288f9178977d8b", build_file = "//third_party:app_crd.BUILD", ) http_archive( name = "ingress-nginx", urls = [ "https://github.com/kubernetes/ingress-nginx/archive/refs/tags/controller-v1.8.0.tar.gz", ], sha256 = "6e571764828b24545eea49582fd56d66d51fc66e52a375d98251c80c57fdb2fc", strip_prefix = "ingress-nginx-controller-v1.8.0", build_file = "//third_party:ingress-nginx.BUILD", ) # -- repo definitions -- # non_module_deps = module_extension(implementation = _non_module_deps_impl) ================================================ FILE: nvchecker.toml ================================================ [__config__] oldver = "current_versions.txt" newver = "new_versions.txt" # containers # git grep -E "^\s+image: " *.yaml | grep -v "{{" [ingress-nginx] source = "container" # As of 2023-06-09, nvchecker is not compatible with registry.k8s.io, which # doesn't return the WWW-Authenticate header that nvchecker expects, so you get # UnsupportedAuthenticationError. registry = "k8s.gcr.io" container = "ingress-nginx/controller" prefix = "v" [oauth2-proxy] source = "container" registry = "quay.io" container = "oauth2-proxy/oauth2-proxy" prefix = "v" [stackdriver-logging-agent] source = "container" registry = "gcr.io" container = "stackdriver-agents/stackdriver-logging-agent" # github packages [cert-manager] source = "github" github = "jetstack/cert-manager" use_latest_release = true prefix = "v" # Does find the version, try alternative at the bottom #[kube-prometheus-stack] #source = "github" #github = "prometheus-community/helm-charts" #path = "charts/kube-prometheus-stack" #use_max_tag = true # TODO(ensonic): requires auth token # use_latest_tag = true #prefix = "kube-prometheus-stack" # Cover helm repos: # https://medium.com/bigdatarepublic/software-versioning-on-kubernetes-806a48480832 ================================================ FILE: scripts/BUILD.bazel ================================================ exports_files([ "common.sh", "config.sh", "include-config.sh", "set-config.sh", ]) ================================================ FILE: scripts/backup_robots.sh ================================================ #!/bin/bash # # Copyright 2021 The Cloud Robotics Authors # # 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. # see https://github.com/kubernetes/kubernetes/issues/90066#issuecomment-780236185 for hiding managed-fields # another tool: https://github.com/itaysk/kubectl-neat DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/common.sh" if ! hash jq 2>/dev/null; then die "This script needs jq (apt install jq)." fi if ! hash yq 2>/dev/null; then die "This script needs yq (pip3 install yq)." fi if [[ -z "${CLOUD_ROBOTICS_CTX}" ]]; then die "CLOUD_ROBOTICS_CTX needs specify the cluster context that shoudl be backed up". fi kc get robots -o yaml | \ yq 2>/dev/null -ry '.items[] | del(.metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"],.metadata.creationTimestamp,.metadata.generation,.metadata.managedFields,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid,.status)' - echo "---" kc get cm -n app-token-vendor -o yaml -l app.kubernetes.io/managed-by=token-vendor | \ yq 2>/dev/null -ry '.items[] | del(.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid)' - ================================================ FILE: scripts/check-images.sh ================================================ #!/bin/bash # # Copyright 2019 The Cloud Robotics Authors # # 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. # # Run with 'json' or 'text' as a first arg to select the format. # # If you don't have the API enabled run (and wait a day to get results): # gcloud --project ${GCP_PROJECT_ID} services enable containeranalysis.googleapis.com # # Postprocessing examples: # - grep "Critical" /tmp/cve-check.demo.txt | sed -e 's/ Critical (\([0-9]*\)):/\1/g' | paste -s -d+ | bc # # Almost all of our images are built with distroless (bazel xxx_image rules). If there are # vulnerabilities, # 1.) check that we are using an up-to-date rules_docker in WORKSPACE. Check upstream # for recent commits that update the distroless base images and if that does not help # 2.) check https://github.com/GoogleContainerTools/distroless/commits/master # for fixes, if there are some, clone rules_docker, run ./update_deps.sh and sent a PR. DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/common.sh" source "${DIR}/include-config.sh" function json { need_delimiter=0 echo "[" for image in $(gcloud container images list --format='csv[no-heading](name)' --repository=${CLOUD_ROBOTICS_CONTAINER_REGISTRY}); do if [[ $need_delimiter == 0 ]]; then need_delimiter=1 else echo "," fi gcloud --project ${GCP_PROJECT_ID} alpha container images describe --show-package-vulnerability --format=json ${image}:${DOCKER_TAG} || \ need_delimiter=0 done echo "]" } function text { for image in $(gcloud container images list --format='csv[no-heading](name)' --repository=${CLOUD_ROBOTICS_CONTAINER_REGISTRY}); do # Filter noise gcloud --project ${GCP_PROJECT_ID} alpha container images describe --show-package-vulnerability ${image}:${DOCKER_TAG} | \ egrep -v '^\s*(registry|repository|digest):' done } if [[ -z "$2" ]]; then die "Usage: $0 " fi include_config "$2" # call arguments verbatim: "$@" ================================================ FILE: scripts/common.sh ================================================ #!/bin/bash # # Copyright 2019 The Cloud Robotics Authors # # 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. function die { echo "$1" >&2 exit 1 } function is_source_install { # This file is present in the root folder only when installing from a binary. [[ ! -e "$(dirname "${BASH_SOURCE[0]}")/../INSTALL_FROM_BINARY" ]] } function log { local project project=$1 shift gcloud logging write cloud-robotics-deploy \ --severity=INFO \ --project=${project} \ --payload-type=json \ "$(cat </dev/null) || saved_ctx="" trap "[[ -n \"${saved_ctx}\" ]] && kubectl config use-context \"${saved_ctx}\"; trap - RETURN" RETURN local location location=$(gcloud container clusters list --filter="name=${name}" --format='value(location)' --project="${project}") case "${location}" in ${region}) gcloud container clusters get-credentials "${name}" \ --region "${region}" \ --project "${project}" \ ;; ${zone}) gcloud container clusters get-credentials "${name}" \ --zone "${zone}" \ --project "${project}" \ ;; esac } # Build GKE context name for existing cluster function gke_context_name { local project project="$1" local cluster_name name="$2" local region region="$3" local zone zone="$4" local location location=$(gcloud container clusters list --filter="name=${name}" --format='value(location)' --project="${project}") if [[ "${location}" == "${zone}" || "${location}" == "${region}" ]]; then echo "gke_${project}_${location}_${name}" fi } function kc { kubectl --context="${CLOUD_ROBOTICS_CTX}" "$@" } ================================================ FILE: scripts/config.sh ================================================ #!/bin/bash # # Copyright 2024 The Cloud Robotics Authors # # 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. # Escapes the input "foo bar" -> "foo\ bar". function escape { sed 's/[^a-zA-Z0-9,._+@%/-]/\\&/g' <<< "$@" } # Escapes the input twice "foo bar" -> "foo\\\ bar" function double_escape { sed 's/[^a-zA-Z0-9,._+@%/-]/\\\\\\&/g' <<< "$@" } # Creates a substitution pattern for sed using an unprintable char as seperator. # This allows the user to use any normal char in the input. function sed_pattern { local regexp="$1" local replacement="$2" echo s$'\001'${regexp}$'\001'${replacement}$'\001' } # Sets the given variable in config.sh. If $value is empty, the variable # assignement is commented out in config.sh. function save_variable { local config_file="$1" local name="$2" local value="$3" if [[ -z "${value}" ]]; then sed -i "s/^\(${name}=.*\)$/#\1/" "${config_file}" elif grep -q "^\(# *\)\{0,1\}${name}=" "${config_file}"; then value=$( double_escape ${value} ) sed -i "$( sed_pattern "^\(# *\)\{0,1\}${name}=.*$" "${name}=${value}" )" "${config_file}" else value=$( escape ${value} ) echo >>"${config_file}" echo "${name}=${value}" >>"${config_file}" fi } ================================================ FILE: scripts/include-config.sh ================================================ #!/usr/bin/env bash # # Copyright 2019 The Cloud Robotics Authors # # 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. # Includes the configuration variables from a config.sh. function check_vars_not_empty { for v in "$@"; do [ -n "${!v}" ] || die "Variable $v is not set or is empty" done } function check_var_is_one_of { local var_name="$1" local allowed_values="${*:2}" local found=false for allowed_value in ${allowed_values}; do if [[ "${!var_name}" = "${allowed_value}" ]]; then found=true fi done if [[ "${found}" = false ]]; then die "Variable ${var_name} has to be one of [${allowed_values}], but was ${!var_name}" fi } function include_config { local project="$1" source <(gcloud storage cat "gs://${project}-cloud-robotics-config/config.sh") # Check that config defines the following set of configuration variables check_vars_not_empty GCP_PROJECT_ID GCP_REGION GCP_ZONE if is_source_install; then # Keep default in sync with src/go/pkg/configutil/config-reader.go CLOUD_ROBOTICS_CONTAINER_REGISTRY=${CLOUD_ROBOTICS_CONTAINER_REGISTRY:-"gcr.io/${GCP_PROJECT_ID}"} SOURCE_CONTAINER_REGISTRY=${CLOUD_ROBOTICS_CONTAINER_REGISTRY} else SOURCE_CONTAINER_REGISTRY=${SOURCE_CONTAINER_REGISTRY:-gcr.io/cloud-robotics-releases} fi CLOUD_ROBOTICS_DEPLOY_ENVIRONMENT=${CLOUD_ROBOTICS_DEPLOY_ENVIRONMENT:-GCP} check_var_is_one_of CLOUD_ROBOTICS_DEPLOY_ENVIRONMENT "GCP" "GCP-testing" GKE_CLUSTER_TYPE=${GKE_CLUSTER_TYPE:-zonal} check_var_is_one_of GKE_CLUSTER_TYPE "zonal" "regional" GKE_DATAPATH_PROVIDER=${GKE_DATAPATH_PROVIDER:-DATAPATH_PROVIDER_UNSPECIFIED} check_var_is_one_of GKE_DATAPATH_PROVIDER "DATAPATH_PROVIDER_UNSPECIFIED" "ADVANCED_DATAPATH" } ================================================ FILE: scripts/migrate.sh ================================================ #!/bin/bash # # Copyright 2025 The Cloud Robotics Authors # # 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. # helper functions to update from older installations # ./migrate.sh DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/common.sh" source "${DIR}/config.sh" source "${DIR}/include-config.sh" set -o pipefail -o errexit # Required or terraform will fail deleting the IoT registry function cleanup_iot_devices { gcloud services list --project="${GCP_PROJECT_ID}" | grep -q cloudiot.googleapis.com || return local iot_registry_name="cloud-robotics" gcloud beta iot registries list --project="${GCP_PROJECT_ID}" --region="${GCP_REGION}" | grep -q "${iot_registry_name}" || return local devices devices=$(gcloud beta iot devices list \ --project "${GCP_PROJECT_ID}" \ --region "${GCP_REGION}" \ --registry "${iot_registry_name}" \ --format='value(id)') if [[ -n "${devices}" ]] ; then echo "Clearing IoT devices from ${iot_registry_name}" 1>&2 for dev in ${devices}; do gcloud beta iot devices delete \ --quiet \ --project "${GCP_PROJECT_ID}" \ --region "${GCP_REGION}" \ --registry "${iot_registry_name}" \ ${dev} done fi } function cleanup_helm_data { # Delete all legacy HELM resources. Do not delete the Helm charts directly, as # we just want to keep the resources and have synk "adopt" them. kc delete cm ready-for-synk 2> /dev/null || true kc delete cm synk-enabled 2> /dev/null || true kc -n kube-system delete deploy tiller-deploy 2> /dev/null || true kc -n kube-system delete service tiller-deploy 2> /dev/null || true kc -n kube-system delete cm -l OWNER=TILLER 2> /dev/null || true } function cleanup_old_cert_manager { # Uninstall and cleanup older versions of cert-manager if needed echo "checking for old cert manager .." kc &>/dev/null get deployments cert-manager || return 0 installed_ver=$(kc get deployments cert-manager -o=go-template --template='{{index .metadata.labels "helm.sh/chart"}}' | rev | cut -d'-' -f1 | rev | tr -d "vV") echo "have cert manager $installed_ver" if [[ "$installed_ver" == 0.5.* ]]; then echo "need to cleanup old version" # see https://docs.cert-manager.io/en/latest/tasks/upgrading/upgrading-0.5-0.6.html#upgrading-from-older-versions-using-helm # and https://docs.cert-manager.io/en/latest/tasks/backup-restore-crds.html # cleanup synk_version=$(kc get resourcesets.apps.cloudrobotics.com --output=name | grep cert-manager | cut -d'/' -f2) echo "deleting resourceset ${synk_version}" ${SYNK} delete ${synk_version} -n default kc delete crd \ certificates.certmanager.k8s.io \ issuers.certmanager.k8s.io \ clusterissuers.certmanager.k8s.io fi if [[ "$installed_ver" == 0.8.* ]]; then echo "need to cleanup old version" # see https://cert-manager.io/docs/installation/upgrading/upgrading-0.8-0.9/ # and https://cert-manager.io/docs/installation/upgrading/upgrading-0.9-0.10/ # cleanup kc delete deployments --namespace default \ cert-manager \ cert-manager-cainjector \ cert-manager-webhook kc delete -n default issuer cert-manager-webhook-ca cert-manager-webhook-selfsign kc delete -n default certificate cert-manager-webhook-ca cert-manager-webhook-webhook-tls kc delete apiservice v1beta1.admission.certmanager.k8s.io fi if [[ "$installed_ver" == 0.10.* ]]; then echo "need to cleanup old version" # cleanup deployments kc delete deployments --namespace default \ cert-manager \ cert-manager-cainjector \ cert-manager-webhook echo "Wait until cert-manager pods are deleted" kc wait pods -l app.kubernetes.io/instance=cert-manager -n default --for=delete --timeout=35s # delete existing cert-manager resources kc delete Issuers,ClusterIssuers,Certificates,CertificateRequests,Orders,Challenges --all-namespaces --all # Delete old webhook ca and tls secrets kc delete secrets --namespace default cert-manager-webhook-ca cert-manager-webhook-tls # cleanup crds kc delete crd \ certificaterequests.certmanager.k8s.io \ certificates.certmanager.k8s.io \ challenges.certmanager.k8s.io \ clusterissuers.certmanager.k8s.io \ issuers.certmanager.k8s.io \ orders.certmanager.k8s.io # cleanup apiservices kc delete apiservices v1beta1.webhook.certmanager.k8s.io fi # This is now installed as part of base-cloud kc delete resourcesets.apps.cloudrobotics.com -l name=cert-manager 2>/dev/null || true } # main if [[ "$#" -lt 2 ]] || [[ ! "$1" =~ ^(cleanup_helm_data|cleanup_old_cert_manager|cleanup_iot_devices)$ ]]; then die "Usage: $0 {cleanup_helm_data|cleanup_old_cert_manager|cleanup_iot_devices} " fi include_config $2 # log and call arguments verbatim: log $2 $0 $1 "$@" ================================================ FILE: scripts/pre-commit ================================================ #!/bin/bash # git hook to ensure code style # ln -sf ../../scripts/pre-commit .git/hooks/ # shellcheck disable=2044,2046 # This script can't handle spaces in filenames. That would be challenging to do # correctly, and we will hopefully never add a filename with a space to the # repository. set -o pipefail result=0 # Allow to call the pre-commit hook with a list of files. This allows to run the # script from the command line like this to check all files: # $ ./scripts/pre-commit $(git ls-files) files="$*" if [ -z "$files" ]; then files="$(git diff --name-only --staged --diff-filter=ACMRTUXB)" fi function files_matching { local include="$1" local exclude="$2" # The diff-filter lists all but deleted files. The `echo` puts the output on # one line for easier copy-pasting. if [[ -z "${exclude}" ]]; then echo $(echo "$files" | tr ' ' '\n' | grep -E "${include}") else echo $(echo "$files" | tr ' ' '\n' | grep -E "${include}" | grep -vE "${exclude}") fi } go_files=$(files_matching "\.go$") if [ -n "$go_files" ]; then # meh, gofmt does not set an exit code # TODO(rodrigoq): this will break if the filenames have spaces diff=$(gofmt -d -e $go_files) if [ -n "$diff" ]; then echo "$diff" files_to_fix=$(gofmt -l $go_files) echo "To fix, run: gofmt -w $files_to_fix" result=1 fi fi py_files=$(files_matching "\.py$") if [ -n "$py_files" ]; then which >/dev/null autopep8 || (echo "Please install autopep8"; exit 1) # TODO(rodrigoq): this will break if the filenames have spaces diff=$(autopep8 -d $py_files) if [ -n "$diff" ]; then echo "$diff" echo "To fix, run: autopep8 -i $py_files" result=1 fi fi build_files=$(echo "$files" | tr ' ' '\n' \ | grep -E "BUILD|WORKSPACE|[.]bzl") if [ -n "$build_files" ]; then which >/dev/null buildifier || (echo "Please install buildifier"; exit 1) diff=$(buildifier -d $build_files) if [ -n "$diff" ]; then echo "$diff" echo "To fix, run: buildifier" \ $(echo $(buildifier -mode=check $build_files | cut -d' ' -f1)) result=1 fi fi # Run Gazelle if a Go or BUILD file changes. This is a heuristic, but hopefully # covers most cases where it is needed. for workspace_dir in $(find -name WORKSPACE -printf "%h\n"); do bzl="$workspace_dir/BUILD.bazel" if [ -e "$bzl" ] && grep -q "gazelle(" "$bzl"; then # This calls gazelle for every workspace, affected or not. if [[ -n "$go_files" || -n "$build_files" ]]; then diff=$(cd ${workspace_dir} && bazel run :gazelle -- -mode=diff 2>/dev/null) if [ -n "$diff" ]; then echo "$diff" echo "To fix:" echo " bazel run :gazelle" result=1 fi fi fi done ts_files=$(files_matching "\.ts$") if [ -n "$ts_files" ]; then which >/dev/null clang-format || (echo "Please install clang-format"; exit 1) diff=$(diff -u <(cat $ts_files) <(clang-format $ts_files)) if [ -n "$diff" ]; then echo "$diff" echo "To fix, run: clang-format -i $ts_files" result=1 fi fi cpp_files=$(files_matching "\.(h|cc)$") if [ -n "$cpp_files" ]; then which >/dev/null clang-format || (echo "Please install clang-format"; exit 1) diff=$(diff -u <(cat $cpp_files) <(clang-format -style=google $cpp_files)) if [ -n "$diff" ]; then echo "$diff" echo "To fix, run: clang-format -style=google -i $cpp_files" result=1 fi fi tf_files=$(files_matching "\.tf$") if [ -n "$tf_files" ]; then if [[ -f WORKSPACE ]] ; then # cloud-robotics bazel build @hashicorp_terraform//:terraform TERRAFORM="${PWD}/bazel-out/../../../external/hashicorp_terraform/terraform" else # infrastructure TERRAFORM="/google/data/ro/teams/terraform/bin/terraform" fi tf_dirs=$(dirname $tf_files | sort | uniq) for tf_dir in $tf_dirs; do if ! ${TERRAFORM} fmt -write=false -list=false -check=true $tf_dir; then ${TERRAFORM} fmt -write=false -list=false -diff=true $tf_dir echo "To fix, run: ${TERRAFORM} fmt $tf_dir" result=1 fi done fi # Run check when either a markdown file changes (it may have new embeddings) or # an example file changes (it may have to be embedded). # TODO(rodrigoq): only run if these files contain an embedmd tag. md_files=$(files_matching "\.md$") # TODO(rodrigoq): find a better way of checking if a file is embedded anywhere. example_files=$(files_matching "example") if [[ -n "$md_files" || -n "$example_files" ]]; then EMBEDMD=${GOPATH:-$HOME/go}/bin/embedmd if [[ ! -f "$EMBEDMD" ]] && ! go install github.com/campoy/embedmd@latest ; then echo "ERROR: embedmd not found and couldn't be installed." >&2 result=1 else # An unchanged .md file may still have changes in the embedded files. all_md_files=$(git ls-files | grep --color=never '\.md$') diff=$($EMBEDMD -d $all_md_files) if [[ $? -ne 0 ]] ; then echo "$diff" echo "To fix, run: $EMBEDMD -w \$(git ls-files | grep --color=never '\\.md$')" result=1 fi fi fi SHELLCHECK_DIR="$HOME/.cache/cloud-robotics" SHELLCHECK="${SHELLCHECK_DIR}/shellcheck-v0.6.0/shellcheck" sh_files=$(files_matching "(\.sh$|pre-commit$)" "deployments/.*/config.sh") # TODO(rodrigoq): enable shellcheck in the apps repo. if [[ -n "$sh_files" && "$(basename "$PWD")" != "apps" ]] ; then if [[ ! -e "$SHELLCHECK" ]] ; then mkdir -p "$SHELLCHECK_DIR" curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.6.0/shellcheck-v0.6.0.linux.x86_64.tar.xz \ | tar -C "$SHELLCHECK_DIR" -xJf - || exit 1 fi # Note: using a lower severity than `warning` is quite noisy. # SC1090 complains not being able able to follow `source "${DIR}/scripts/common.sh"`, # which is a common pattern and we don't benefit from shellcheck following the # `source` statement. if ! "$SHELLCHECK" --severity=warning -e SC1090 --external-sources $sh_files ; then echo "ERROR: shellcheck found issues. These need to be fixed manually." >&2 result=1 fi fi exit $result ================================================ FILE: scripts/robot-sim.sh ================================================ #!/bin/bash # # Copyright 2019 The Cloud Robotics Authors # # 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. # Manage simulated robots # # Simulated robots are implemented as a separate cluster, running the same # components like a physical robot in addition to the robot simulator. DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${DIR}/common.sh" source "${DIR}/include-config.sh" set -o pipefail -o errexit function set_defaults { local GCP_PROJECT_ID="$1" include_config "${GCP_PROJECT_ID}" if [[ -z "${ROBOT_LABELS}" ]]; then ROBOT_LABELS="simulated=true" fi } function create { local GCP_PROJECT_ID="$1" local ROBOT_NAME="$2" local ROBOT_TYPE="${3:-mir-100}" set_defaults "${GCP_PROJECT_ID}" local GKE_SIM_CONTEXT="gke_${GCP_PROJECT_ID}_${GCP_ZONE}_${ROBOT_NAME}" # Create cloud cluster for robot simulation unless already exists. # To more accurately simulate a robot cluster, this uses the # robot-service@ service account instead of enabling Workload Identity, as we # don't have any on-prem/robot equivalent to that. gcloud >/dev/null 2>&1 container clusters describe "${ROBOT_NAME}" \ --zone=${GCP_ZONE} --project=${GCP_PROJECT_ID} || \ gcloud container clusters create "${ROBOT_NAME}" \ --enable-legacy-authorization \ --machine-type="e2-standard-2" \ --num-nodes=1 \ --max-nodes=2 \ --enable-ip-alias \ --issue-client-certificate \ --no-enable-basic-auth \ --metadata disable-legacy-endpoints=true \ --scopes gke-default,cloud-platform \ --service-account "robot-service@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \ --zone=${GCP_ZONE} \ --project=${GCP_PROJECT_ID} gke_get_credentials "${GCP_PROJECT_ID}" "${ROBOT_NAME}" "${GCP_REGION}" "${GCP_ZONE}" POD_CIDR=$(gcloud container clusters describe "${ROBOT_NAME}" \ --project=${GCP_PROJECT_ID} \ --zone=${GCP_ZONE} \ | grep podIpv4CidrBlock | awk '{print $2;}') # shellcheck disable=2097 disable=2098 KUBE_CONTEXT=${GKE_SIM_CONTEXT} \ HOST_HOSTNAME="nic0.${ROBOT_NAME}${GCP_ZONE}.c.${GCP_PROJECT_ID}.internal.gcpnode.com" \ ACCESS_TOKEN=$(gcloud auth application-default print-access-token) \ $DIR/../src/bootstrap/robot/setup_robot.sh \ ${ROBOT_NAME} \ --project ${GCP_PROJECT_ID} \ --robot-type "${ROBOT_TYPE}" \ --fluentd=false \ --fluentbit=false \ --running-on-gke=true \ --pod-cidr "${POD_CIDR}" \ --labels "${ROBOT_LABELS}" } function delete { local GCP_PROJECT_ID="$1" local ROBOT_NAME="$2" set_defaults "${GCP_PROJECT_ID}" kubectl --context=${CLOUD_ROBOTICS_CTX} delete robots.registry.cloudrobotics.com "${ROBOT_NAME}" || true gcloud container clusters delete "${ROBOT_NAME}" \ --zone=${GCP_ZONE} --project=${GCP_PROJECT_ID} } # Alias for create. function update { create "$@" } # main if [[ "$#" -lt 3 ]]; then die "Usage: $0 {create|delete|update} []" fi # call arguments verbatim: "$@" ================================================ FILE: scripts/set-config.sh ================================================ #!/bin/bash # # Copyright 2019 The Cloud Robotics Authors # # 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. # TODO(skopecki) # * Consider setting CLOUD_ROBOTICS_SHARED_OWNER_GROUP and # APP_MANAGEMENT as well. set -o pipefail -o errexit DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" source "${DIR}/scripts/common.sh" source "${DIR}/scripts/config.sh" # Reads a variable from user input. function read_variable { local target_var="$1" local question="$2" local default="$3" echo echo "${question}" if [[ -n "${default}" ]]; then echo -n "[ENTER] for \"${default}\": " fi read -er input if [[ -z "${input}" ]]; then # shellcheck disable=SC2046 eval ${target_var}=$( escape ${default} ) else # shellcheck disable=SC2046 eval ${target_var}=$( escape ${input} ) fi } # Outputs the variable to the user. function print_variable { local description="$1" local value="$2" if [[ -n "${value}" ]]; then echo "${description}: ${value}" fi } # Asks a yes/no question and returns the mapped input. function ask_yn { local question="$1" local default="$2" echo echo -n "$question" if [[ "${default}" = "n" ]]; then echo -n " [yN] " else echo -n " [Yn] " fi while true; do read -n 1 input if [[ -z "${input}" ]]; then if [[ "${default}" = "n" ]]; then return 1 else return 0 fi fi echo if [[ "${input}" =~ y|Y ]]; then return 0 elif [[ "${input}" =~ n|N ]]; then return 1 fi echo -n "Please answer with 'y' or 'n'. " done } # Parse flags. if [[ ! "$1" = --* ]]; then GCP_PROJECT_ID="$1" fi for arg in "$@"; do if [[ "${arg}" = "--ensure-config" ]]; then FLAG_ENSURE_CONFIG=1 elif [[ "${arg}" = "--edit-oauth" ]]; then FLAG_EDIT_OAUTH=1 fi done if [[ -z "${GCP_PROJECT_ID}" ]]; then echo echo "Usage: $0 []" echo "Supported options:" echo " --ensure-config Does nothing if a config exists already." echo " --edit-oauth Enables and configures OAuth." die fi # Load config if it exists. CLOUD_BUCKET="gs://${GCP_PROJECT_ID}-cloud-robotics-config" CONFIG_FILE="$(mktemp)" trap '{ rm -f ${CONFIG_FILE}; }' EXIT if gcloud storage cp "${CLOUD_BUCKET}/config.sh" "${CONFIG_FILE}" 2>/dev/null; then if [[ -n "${FLAG_ENSURE_CONFIG}" ]]; then echo "Found Cloud Robotics config." exit 0 fi source ${CONFIG_FILE} else if [[ -n "${FLAG_EDIT_OAUTH}" ]]; then die "You have to create a config before you can enable OAuth." fi cp ${DIR}/config.sh.tmpl ${CONFIG_FILE} fi # Check that the project exists and we have access. gcloud projects describe "${GCP_PROJECT_ID}" >/dev/null \ || die "ERROR: unable to access Google Cloud project: ${GCP_PROJECT_ID}" function set_default_vars { # Enable Compute Engine API which is necessary to validate the zones. if ! gcloud services list --enabled --project ${GCP_PROJECT_ID} \ | grep "^compute.googleapis.com \+" >/dev/null; then # TODO(skopecki) This can take a minute. Find a better solution to verify compute zones. echo "Enabling Compute Engine API..." gcloud services enable compute.googleapis.com --project ${GCP_PROJECT_ID} fi # Ask for region and zone. GCP_ZONE=${GCP_ZONE:-"europe-west1-c"} read_variable GCP_ZONE "In which zone should Cloud Robotics be deployed?" "${GCP_ZONE}" # Verify the zone exists. gcloud compute zones list -q --project "${GCP_PROJECT_ID}" --uri | grep -q "zones/${GCP_ZONE}$" \ || die "ERROR: the zone does not exist in your project: ${GCP_ZONE}" GCP_REGION=${GCP_ZONE%-?} # Ask for gke cluster type GKE_CLUSTER_TYPE="zonal" while :; do read_variable GKE_CLUSTER_TYPE "Should the cluster be 'zonal' or 'regional'?" "${GKE_CLUSTER_TYPE}" if [[ "${GKE_CLUSTER_TYPE}" == "zonal" || "${GKE_CLUSTER_TYPE}" == "regional" ]]; then break fi echo "Value must be one of: 'zonal','regional'" done # Use dataplane_v2 for all new projects GKE_DATAPATH_PROVIDER="ADVANCED_DATAPATH" # Ask for Terraform bucket and location. OLD_TERRAFORM_GCS_BUCKET="${TERRAFORM_GCS_BUCKET}" OLD_TERRAFORM_GCS_PREFIX="${TERRAFORM_GCS_PREFIX}" if [[ -z "${TERRAFORM_GCS_BUCKET}" ]]; then OLD_TF_LOCATION="${CLOUD_BUCKET}/terraform" else OLD_TF_LOCATION="gs://${TERRAFORM_GCS_BUCKET}/${TERRAFORM_GCS_PREFIX}" fi read_variable TF_LOCATION "In which GCP storage folder should your Terraform state be stored?" \ "${OLD_TF_LOCATION}" TF_LOCATION_REGEX='^\(gs://\)\?\([-_a-zA-Z0-9]\+\)\(\/\(.*[^/]\)\)\?/\?$' TERRAFORM_GCS_BUCKET=$( echo "${TF_LOCATION}" | sed "s#${TF_LOCATION_REGEX}#\2#;q" ) \ || die "ERROR: Invalid GCP storage folder. Accepted format: gs:///" TERRAFORM_GCS_PREFIX=$( echo "${TF_LOCATION}" | sed "s#${TF_LOCATION_REGEX}#\4#;q" ) # Docker registries. if is_source_install; then read_variable CLOUD_ROBOTICS_CONTAINER_REGISTRY \ "Which Docker registry do you want to use when installing from sources? Use \"default\" for gcr.io/${GCP_PROJECT_ID}." \ "${CLOUD_ROBOTICS_CONTAINER_REGISTRY:-default}" if [[ "${CLOUD_ROBOTICS_CONTAINER_REGISTRY}" == "default" ]]; then CLOUD_ROBOTICS_CONTAINER_REGISTRY= fi fi # TODO(skopecki) If CLOUD_ROBOTICS_CONTAINER_REGISTRY is private and does not belongs to this GCR project, # it could be added automatically to PRIVATE_DOCKER_PROJECTS. read_variable PRIVATE_DOCKER_PROJECTS \ "Do you need to read private Docker images from a GCR project? Space-separated list of alphanumeric project ids, or \"none\" for none." \ "${PRIVATE_DOCKER_PROJECTS:-none}" if [[ "${PRIVATE_DOCKER_PROJECTS}" == "none" ]]; then PRIVATE_DOCKER_PROJECTS= fi # Certificate provider set_certificate_provider_vars } function set_oauth_vars { echo "Follow https://googlecloudrobotics.github.io/core/how-to/setting-up-oauth.html to obtain OAuth client id and secret." read_variable CLOUD_ROBOTICS_OAUTH2_CLIENT_ID "Enter OAuth client id." \ "${CLOUD_ROBOTICS_OAUTH2_CLIENT_ID}" read_variable CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET "Enter OAuth client secret." \ "${CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET}" if [[ -z "${CLOUD_ROBOTICS_COOKIE_SECRET}" ]] ||\ ask_yn "Generate new cookie secret?" "n"; then CLOUD_ROBOTICS_COOKIE_SECRET="$( head -c 16 /dev/urandom | base64 )" fi } function set_certificate_provider_vars { CA_OPTIONS="lets-encrypt, google-cas" CA_DEFAULT="lets-encrypt" # Select provider read_variable CLOUD_ROBOTICS_CERTIFICATE_PROVIDER \ "Select the certificate provider. Should be one of: ${CA_OPTIONS}." \ "${CLOUD_ROBOTICS_CERTIFICATE_PROVIDER:-${CA_DEFAULT}}" # Request certificate configuration if the provider requires it if [[ ! "lets-encrypt" =~ (" "|^)"${CLOUD_ROBOTICS_CERTIFICATE_PROVIDER}"(" "|$) ]]; then set_certificate_vars fi } function set_certificate_vars { echo "Configuring certificate information." echo "Refer to RFC 4519 for explanations of the fields: https://datatracker.ietf.org/doc/html/rfc4519#section-2" read_variable CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION \ "Organization (O)" \ "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION:-${GCP_PROJECT_ID}}" read_variable CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME \ "Common Name (CN)" \ "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME:-${GCP_PROJECT_ID}}" read_variable CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATIONAL_UNIT \ "(Optional) Organizational Unit (OU)" \ "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATIONAL_UNIT}" } if [[ -n "${FLAG_EDIT_OAUTH}" ]]; then set_oauth_vars else set_default_vars fi # Output configuration before saving. echo echo " Your configuration" echo "========================" print_variable "GCP project ID" "${GCP_PROJECT_ID}" print_variable "GCP region" "${GCP_REGION}" print_variable "GCP zone" "${GCP_ZONE}" print_variable "GKE cluster type" "${GKE_CLUSTER_TYPE}" print_variable "GKE datapath provider" "${GKE_DATAPATH_PROVIDER}" print_variable "Terraform state bucket" "${TERRAFORM_GCS_BUCKET}" print_variable "Terraform state directory" "${TERRAFORM_GCS_PREFIX}" print_variable "Docker container registry" "${CLOUD_ROBOTICS_CONTAINER_REGISTRY}" print_variable "Projects for private Docker images" "${PRIVATE_DOCKER_PROJECTS}" print_variable "OAuth client id" "${CLOUD_ROBOTICS_OAUTH2_CLIENT_ID}" print_variable "OAuth client secret" "${CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET}" print_variable "OAuth cookie secret" "${CLOUD_ROBOTICS_COOKIE_SECRET}" print_variable "Certificate provider" "${CLOUD_ROBOTICS_CERTIFICATE_PROVIDER}" print_variable "Certificate Subject Organization (O)" "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION}" print_variable "Certificate Subject Common Name (CN)" "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME}" print_variable "Certificate Subject Organizational Unit (OU)" "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATIONAL_UNIT}" if ! ask_yn "Would you like to save this configuration?"; then exit 0 fi if [[ -n "${OLD_TERRAFORM_GCS_BUCKET}" &&\ ( ! "${OLD_TERRAFORM_GCS_BUCKET}" = "${TERRAFORM_GCS_BUCKET}" ||\ ! "${OLD_TERRAFORM_GCS_PREFIX}" = "${TERRAFORM_GCS_PREFIX}") ]]; then # Copy Terraform state to new location. echo "Copying Terraform state..." gcloud storage cp "gs://${OLD_TERRAFORM_GCS_BUCKET}/${OLD_TERRAFORM_GCS_PREFIX}/*.tfstate" \ "gs://${TERRAFORM_GCS_BUCKET}/${TERRAFORM_GCS_PREFIX}/" fi # Save all parameter values. echo echo "Saving configuration..." save_variable "${CONFIG_FILE}" GCP_PROJECT_ID "${GCP_PROJECT_ID}" save_variable "${CONFIG_FILE}" GCP_REGION "${GCP_REGION}" save_variable "${CONFIG_FILE}" GCP_ZONE "${GCP_ZONE}" if [[ "${GKE_CLUSTER_TYPE}" == "regional" ]]; then save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CTX "gke_${GCP_PROJECT_ID}_${GCP_REGION}_cloud-robotics" else save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CTX "gke_${GCP_PROJECT_ID}_${GCP_ZONE}_cloud-robotics" fi save_variable "${CONFIG_FILE}" GKE_CLUSTER_TYPE "${GKE_CLUSTER_TYPE}" save_variable "${CONFIG_FILE}" GKE_DATAPATH_PROVIDER "${GKE_DATAPATH_PROVIDER}" save_variable "${CONFIG_FILE}" TERRAFORM_GCS_BUCKET "${TERRAFORM_GCS_BUCKET}" save_variable "${CONFIG_FILE}" TERRAFORM_GCS_PREFIX "${TERRAFORM_GCS_PREFIX}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CONTAINER_REGISTRY "${CLOUD_ROBOTICS_CONTAINER_REGISTRY}" save_variable "${CONFIG_FILE}" PRIVATE_DOCKER_PROJECTS "${PRIVATE_DOCKER_PROJECTS}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_OAUTH2_CLIENT_ID "${CLOUD_ROBOTICS_OAUTH2_CLIENT_ID}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET "${CLOUD_ROBOTICS_OAUTH2_CLIENT_SECRET}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_COOKIE_SECRET "${CLOUD_ROBOTICS_COOKIE_SECRET}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CERTIFICATE_PROVIDER "${CLOUD_ROBOTICS_CERTIFICATE_PROVIDER}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATION}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_COMMON_NAME}" save_variable "${CONFIG_FILE}" CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATIONAL_UNIT "${CLOUD_ROBOTICS_CERTIFICATE_SUBJECT_ORGANIZATIONAL_UNIT}" # Upload config to the cloud. if ! gcloud -q storage buckets describe --project ${GCP_PROJECT_ID} "${CLOUD_BUCKET}" >/dev/null 2>&1; then gcloud storage buckets create --project ${GCP_PROJECT_ID} ${CLOUD_BUCKET} fi gcloud storage mv "${CONFIG_FILE}" "${CLOUD_BUCKET}/config.sh" ================================================ FILE: src/.gitignore ================================================ .gopath ================================================ FILE: src/BUILD.bazel ================================================ exports_files( srcs = ["go.mod"], ) ================================================ FILE: src/README.md ================================================ # Source Code This directory contains the source code for Cloud Robotics Core components. Most components are written in Go. ## Go The [Gazelle](https://github.com/bazelbuild/bazel-gazelle) tool manages bazel BUILD files for Go. ### Dependencies To automatically update dependencies in bazel BUILD files run: ``` bazel run //:gazelle ``` To re-generate Go modules dependencies run this from the top-level source directory: ``` ./src/gomod.sh ``` This will always download the latest stable tag of a go module. To use a specific version run eg:: ``` cd src # use an older version, that the latest stable go get -d github.com/mitchellh/go-server-timing@v1.0.1 # use a specific, yet untagged version go get -d github.com/mitchellh/go-server-timing@feb680ab92c20d57c527399b842e1941bde888c3 # to also upgrade dependencies, use: go get -d -u github.com/mitchellh/go-server-timing@v1.0.1 ``` More tips on this [one-pager](https://encore.dev/guide/go.mod) ### Licenses Install go-license: ``` go install github.com/google/go-licenses@latest ``` run it: ``` cd src # get a vsc of all licenses ~/go/bin/go-licenses csv . # check for bad (forbidden) licenses, should be empty ~/go/bin/go-licenses check ./... ``` ### Docs In order to force a new snapshot, run ```bash VERSION=$(curl -s https://proxy.golang.org/github.com/googlecloudrobotics/core/@latest | jq -r ".Version") echo "https://pkg.go.dev/github.com/googlecloudrobotics/core/src/go@${VERSION}" ``` and open the printed link. Then that version is part of the history. ## third party We track some external deps through [nvchecker](https://github.com/lilydjwg/nvchecker). Get the tool by running: ```shell pip3 install nvchecker ``` Below are sample commands for the common workflows. Run all those from the root of the repo. Add new dependency by adding a blob to nvchecker.toml: ```toml [ingress-nginx] source = "container" registry = "k8s.gcr.io" container = "ingress-nginx/controller" prefix = "v" ``` Get initial version (use same command to update the version): ```shell $ nvtake -c nvchecker.toml ingress-nginx=0.44.0 ``` After updating, please also manually keep METADATA file in sync. Check for updates: ```shell $ nvchecker -c nvchecker.toml [I 09-06 12:26:20.253 core:354] ingress-nginx: updated from 0.44.0 to 1.0.0 ``` ================================================ FILE: src/app_charts/BUILD.bazel ================================================ load("//bazel/build_rules/app_chart:cache_gcr_credentials.bzl", "cache_gcr_credentials") load("//bazel/build_rules/app_chart:run_parallel.bzl", "run_parallel") # base is not in this list because it's not an app, but installed # manually. APPS = [ "k8s-relay", "mission-crd", "prometheus", "token-vendor", "akri", ] run_parallel( name = "push-cached-credentials", targets = [ "//src/app_charts/base:base-cloud.push", "//src/app_charts/base:base-robot.push", "//src/app_charts/platform-apps:platform-apps-cloud.push", ] + [ "//src/app_charts/{app}:{app}.push".format(app = a) for a in APPS ], ) cache_gcr_credentials( name = "push", target = "push-cached-credentials", ) filegroup( name = "app_resources", srcs = ["//src/app_charts/{app}:{app}.yaml".format(app = a) for a in APPS], visibility = ["//visibility:public"], ) ================================================ FILE: src/app_charts/README.md ================================================ # Testing instructions Use `bazel run :push` on the app directory to build & upload Docker images, upload Helm charts and update the app manifest: ```bash bazel run //src/app_charts/k8s-relay:k8s-relay.push ``` # Apps ## Mission CRD The Mission CRD App creates the mission custom resource definition in the cloud and on the robot. The mission custom resources are used to send commands to the robot and are actuated by a robot-type-specific controller. ================================================ FILE: src/app_charts/akri/BUILD.bazel ================================================ load("//bazel:app.bzl", "app") load("//bazel:app_chart.bzl", "app_chart") load("//bazel:build_rules/helm_template.bzl", "helm_template") helm_template( name = "akri-chart.robot", chart = "//third_party/akri:akri-0.12.9.tgz", helm_version = 3, # The namespace will later be replaced with the actual one. namespace = "HELM-NAMESPACE", release_name = "akri", values = "akri-robot.values.yaml", ) app_chart( name = "akri-robot", extra_templates = [ "//third_party/akri:akri-configuration-crd.yaml", "//third_party/akri:akri-instance-crd.yaml", ], files = [ ":akri-chart.robot", ], values = "values-robot.yaml", ) app( name = "akri", charts = [ ":akri-robot", ], visibility = ["//visibility:public"], ) ================================================ FILE: src/app_charts/akri/akri-robot.values.yaml ================================================ # kubernetesDistro describes the Kubernetes distro Akri is running on. It is used to conditionally set # distribution specific values such as container runtime socket. Options: microk8s | k3s | k8s kubernetesDistro: k8s # enable udev support for usb devices udev: discovery: enabled: true configuration: enabled: true name: akri-udev discoveryDetails: udevRules: ${UDEV_RULES} ================================================ FILE: src/app_charts/akri/robot/akri.yaml ================================================ # This includes all resources expanded from the akri chart using # the values in ../values.yaml. # Some pseudo-variables that were inserted there are replaced with actual runtime values. {{ .Files.Get "files/akri-chart.robot.yaml" | replace "HELM-NAMESPACE" .Release.Namespace | replace "${UDEV_RULES}" (toJson .Values.udev.rules) }} ================================================ FILE: src/app_charts/akri/values-robot.yaml ================================================ domain: "example.com" project: "my-gcp-project" registry: "gcr.io/my-gcp-project" robots: [] udev: rules: [] ================================================ FILE: src/app_charts/base/BUILD.bazel ================================================ load("@rules_shell//shell:sh_test.bzl", "sh_test") load("//bazel:app_chart.bzl", "app_chart") load("//bazel:build_rules/helm_template.bzl", "helm_template") # Tests app_chart( name = "base-test", extra_templates = [ ":cloud/namespace.yaml", ":cloud/apps-crd.yaml", ":robot/app-management.yaml", ":robot/cert-manager.yaml", ":robot/cert-manager-certificates.yaml", ":robot/cert-manager-issuers.yaml", ], files = [ ":cert-manager-chart.robot", ], images = { "chart-assignment-controller": "//src/go/cmd/chart-assignment-controller:chart-assignment-controller-image", }, values = ":values-robot.yaml", visibility = ["//visibility:public"], ) sh_test( name = "app_management_test", srcs = ["app_management_test.sh"], data = [ ":base-cloud", ":base-robot", "@kubernetes_helm//:helm", ], ) # Robot helm_template( name = "cert-manager-chart.robot", chart = "//third_party/cert-manager:cert-manager-v1.16.3.tgz", helm_version = 3, # The namespace will later be replaced with the actual one. namespace = "HELM-NAMESPACE", release_name = "cert-manager", values = "cert-manager-robot.values.yaml", ) app_chart( name = "base-robot", extra_templates = [ ":cloud/namespace.yaml", ":cloud/registry-crd.yaml", ":cloud/apps-crd.yaml", "//third_party/kube-prometheus-stack:01-crds.yaml", ], files = [ ":cert-manager-chart.robot", "//third_party/fluentd_gcp_addon", ], images = { "cr-syncer": "//src/go/cmd/cr-syncer:cr-syncer-image", "gcr-credential-refresher": "//src/go/cmd/gcr-credential-refresher:gcr-credential-refresher-image", "metadata-server": "//src/go/cmd/metadata-server:metadata-server-image", "chart-assignment-controller": "//src/go/cmd/chart-assignment-controller:chart-assignment-controller-image", }, values = "values-robot.yaml", visibility = ["//visibility:public"], ) # Cloud helm_template( name = "cert-manager-chart.cloud", chart = "//third_party/cert-manager:cert-manager-v1.16.3.tgz", helm_version = 3, # The namespace will later be replaced with the actual one. namespace = "HELM-NAMESPACE", release_name = "cert-manager", values = "cert-manager-cloud.values.yaml", ) helm_template( name = "cert-manager-google-cas-issuer-chart.cloud", chart = "//third_party/cert-manager-google-cas-issuer:cert-manager-google-cas-issuer-v0.6.2.tgz", # The namespace will later be replaced with the actual one. namespace = "HELM-NAMESPACE", release_name = "cert-manager-google-cas-issuer", values = "cert-manager-google-cas-issuer-cloud.values.yaml", ) app_chart( name = "base-cloud", extra_templates = [ "@com_github_kubernetes_sigs_application//:app_crd", "//third_party/kube-prometheus-stack:01-crds.yaml", ], files = [ "relay-dashboard.json", ":cert-manager-chart.cloud", ":cert-manager-google-cas-issuer-chart.cloud", "@ingress-nginx//:ingress-nginx-dashboards", ], images = { "app-rollout-controller": "//src/go/cmd/app-rollout-controller:app-rollout-controller-image", "chart-assignment-controller": "//src/go/cmd/chart-assignment-controller:chart-assignment-controller-image", "cr-syncer-auth-webhook": "//src/go/cmd/cr-syncer-auth-webhook:cr-syncer-auth-webhook-image", }, values = "values-cloud.yaml", visibility = ["//visibility:public"], ) ================================================ FILE: src/app_charts/base/README.md ================================================ # Making changes to the `fluent-bit` configmap If you want to change the `fluent-bit` spec, do not edit the autogenerated file [`./robot/fluent-bit.yaml`](./robot/fluent-bit.yaml)! Instead, edit [`./fluent-bit-values.yaml`](./fluent-bit-values.yaml) and run [`./fluent-bit-helm.sh`](./fluent-bit-helm.sh) afterwards. ================================================ FILE: src/app_charts/base/app_management_test.sh ================================================ #!/usr/bin/env bash # # Copyright 2019 The Cloud Robotics Authors # # 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. HELM="${TEST_SRCDIR}/+non_module_deps+kubernetes_helm/helm" if [[ ! -x "${HELM}" ]] ; then # If we hit this again, consider using the runfiles library: # https://github.com/bazelbuild/bazel/blob/master/tools/bash/runfiles/runfiles.bash#L55-L86 echo >&2 "Failed to locate helm in ${TEST_SRCDIR}." exit 1 fi CLOUD_BASE="${TEST_SRCDIR}/_main/src/app_charts/base/base-cloud-0.0.1.tgz" ROBOT_BASE="${TEST_SRCDIR}/_main/src/app_charts/base/base-robot-0.0.1.tgz" function test_failed() { echo "TEST FAILED: $1" exit 1 } function test_passed() { echo "TEST PASSED: $1" } function expect_app_installed() { local command="$1" local application="$2" local template if ! template=$(${command}); then test_failed "\"${command}\" failed" fi if [[ "${template}" != *"app: ${application}"* ]]; then echo "TEMPLATE: ${template}" test_failed "expected \"${application}\" to be installed in template created by \"${command}\"" fi test_passed "application \"${application}\" is included in template created by \"${command}\"" } function expect_app_not_installed() { local command="$1" local application="$2" local template if ! template=$(${command}); then echo "TEMPLATE: ${template}" test_failed "\"${command}\" failed" fi if [[ "${template}" == *"app: ${application}"* ]]; then test_failed "did not expected \"${application}\" to be installed in template created by \"${command}\"" fi test_passed "application \"${application}\" is not included in template created by \"${command}\"" } expect_app_installed "${HELM} template ${CLOUD_BASE} --set-string app_management=true" "app-rollout-controller" expect_app_installed "${HELM} template ${CLOUD_BASE} --set-string app_management=true" "chart-assignment-controller" expect_app_not_installed "${HELM} template ${CLOUD_BASE} --set-string app_management=false" "app-rollout-controller" expect_app_not_installed "${HELM} template ${CLOUD_BASE} --set-string app_management=false" "chart-assignment-controller" expect_app_installed "${HELM} template ${ROBOT_BASE} --set-string app_management=true" "chart-assignment-controller" expect_app_not_installed "${HELM} template ${ROBOT_BASE} --set-string app_management=false" "chart-assignment-controller" ================================================ FILE: src/app_charts/base/cert-manager-cloud.values.yaml ================================================ # Configuration for the cert-manager chart. # Reference: https://github.com/jetstack/cert-manager/blob/master/deploy/charts/cert-manager/values.yaml # Install CRDs by helm chart that webhook works in different namespace as cert-manager installCRDs: true # Enable Workload Identity for DNS01 support when we have a custom domain. serviceAccount: annotations: iam.gke.io/gcp-service-account: cert-manager@PROJECT-ID.iam.gserviceaccount.com ================================================ FILE: src/app_charts/base/cert-manager-google-cas-issuer-cloud.values.yaml ================================================ # Configuration for the cert-manager chart. # Reference: https://github.com/jetstack/google-cas-issuer/blob/main/deploy/charts/google-cas-issuer/values.yaml # No values are required for now. # This was put in place to add values in the future without requiring additional configuration in other files. # If values are added this disclaimer should be removed. # The Kubernetes service account must be annotated in order to impersonate a GCP service account using workload identity. serviceAccount: annotations: # PROJECT-ID will be replaced by a script in a future step with the contents of the `PROJECT_ID` env var. iam.gke.io/gcp-service-account: sa-google-cas-issuer@PROJECT-ID.iam.gserviceaccount.com app: approval: subjects: - kind: ServiceAccount name: cert-manager # TODO(alejoasd): this should be set from configuration dynamically namespace: default ================================================ FILE: src/app_charts/base/cert-manager-robot.values.yaml ================================================ # Configuration for the cert-manager chart. # Reference: https://github.com/jetstack/cert-manager/blob/master/deploy/charts/cert-manager/values.yaml global: # Reduce verbosity of text-logging. logLevel: 1 # Install CRDs by helm chart that webhook works in different namespace as cert-manager installCRDs: true # Disable leader-elect for cert-manager and cainjector. # Since we only have one replica running, we don't # need leader election to ensure only one instance is active. # By turning this off, the leader will not update its leases in etcd every N seconds which # ultimately reduces etcd disk writes. # # To ensure that we only have one replica running, we need to use the Recreate # deployment strategy. extraArgs: - --leader-elect=false strategy: type: Recreate cainjector: extraArgs: - --leader-elect=false strategy: type: Recreate ================================================ FILE: src/app_charts/base/cloud/app-management-policy.yaml ================================================ # This policy lets app-rollout and chart-assigment controllers operate on the # apps & registry CRDs. It also grants chart-assignment-controller the # cluster-admin role, which lets it install any Helm chart. This is a very # broad role, but since many Helm charts install ClusterRoleBindings it's not # possible to do it without cluster-admin or something equivalent. # For app-rollout-controller apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:app-rollout-controller:base labels: app-rollout-controller.cloudrobotics.com/aggregate-to-app-rollout: "true" rules: - apiGroups: - registry.cloudrobotics.com resources: - robots - robots/status verbs: - get - list - watch - apiGroups: - apps.cloudrobotics.com resources: - apps - approllouts - approllouts/status - chartassignments - chartassignments/status verbs: - get - list - watch - apiGroups: - apps.cloudrobotics.com resources: - chartassignments verbs: - create - update - patch - delete - apiGroups: - apps.cloudrobotics.com resources: - approllouts/status verbs: - update - patch --- # Aggregated role for app-rollout-controller apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:app-rollout-controller aggregationRule: clusterRoleSelectors: - matchLabels: app-rollout-controller.cloudrobotics.com/aggregate-to-app-rollout: "true" rules: [] # The control plane automatically fills in the rules --- apiVersion: v1 kind: ServiceAccount metadata: name: app-rollout-controller --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cloud-robotics:app-rollout-controller roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cloud-robotics:app-rollout-controller subjects: - namespace: {{ .Release.Namespace }} kind: ServiceAccount name: app-rollout-controller --- # For chart-assignment-controller apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:chart-assignment-controller:base labels: chart-assignment-controller.cloudrobotics.com/aggregate-to-chart-assignment: "true" rules: - apiGroups: - apps.cloudrobotics.com resources: - chartassignments - chartassignments/status verbs: - get - list - watch - apiGroups: - apps.cloudrobotics.com resources: - chartassignments/status verbs: - update - patch - apiGroups: - apps.cloudrobotics.com resources: - resourcesets - resourcesets/status verbs: - get - list - watch - create - update - patch - delete --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:chart-assignment-controller:namespace-admin labels: chart-assignment-controller.cloudrobotics.com/aggregate-to-chart-assignment: "true" rules: - apiGroups: - "" resources: - namespaces verbs: - get - list - watch - create - update - patch - delete --- # Aggregated role for chart-assignment-controller apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:chart-assignment-controller aggregationRule: clusterRoleSelectors: - matchLabels: chart-assignment-controller.cloudrobotics.com/aggregate-to-chart-assignment: "true" - matchLabels: rbac.authorization.k8s.io/aggregate-to-admin: "true" rules: [] # The control plane automatically fills in the rules --- apiVersion: v1 kind: ServiceAccount metadata: name: chart-assignment-controller --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cloud-robotics:chart-assignment-controller roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cloud-robotics:chart-assignment-controller subjects: - namespace: {{ .Release.Namespace }} kind: ServiceAccount name: chart-assignment-controller --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cloud-robotics:chart-assignment-controller:cluster-admin roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - namespace: {{ .Release.Namespace }} kind: ServiceAccount name: chart-assignment-controller ================================================ FILE: src/app_charts/base/cloud/app-management.yaml ================================================ {{ if eq .Values.app_management "true" }} # app-rollout-controller apiVersion: apps/v1 kind: Deployment metadata: name: app-rollout-controller spec: replicas: 1 selector: matchLabels: app: app-rollout-controller template: metadata: labels: app: app-rollout-controller spec: serviceAccountName: app-rollout-controller containers: - name: app-rollout-controller image: {{ .Values.registry }}{{ .Values.images.app_rollout_controller }} args: - "--params=\ domain={{ .Values.domain }},\ project={{ .Values.project }},\ ingress_ip={{ .Values.ingress_ip }},\ registry={{ .Values.registry }},\ deploy_environment={{ .Values.deploy_environment }},\ region={{ .Values.region }},\ use_tv_verbose={{ .Values.use_tv_verbose }}" - --webhook-port=9876 - --cert-dir=/tls env: - name: GOOGLE_CLOUD_PROJECT value: {{ .Values.project }} livenessProbe: initialDelaySeconds: 15 periodSeconds: 10 httpGet: port: 8080 path: /healthz ports: - name: webhook containerPort: 9876 volumeMounts: - mountPath: /tls name: tls securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 volumes: - name: tls secret: secretName: app-rollout-controller-tls --- apiVersion: v1 kind: Service metadata: name: app-rollout-controller spec: type: ClusterIP ports: - port: 443 targetPort: webhook selector: app: app-rollout-controller --- # The app rollout controller runs admission webhooks, which need to be served via TLS. apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: app-rollout-controller spec: secretName: app-rollout-controller-tls commonName: app-rollout-controller.{{ .Release.Namespace }}.svc dnsNames: - app-rollout-controller.{{ .Release.Namespace }}.svc - app-rollout-controller.{{ .Release.Namespace }}.svc.cluster.local issuerRef: kind: ClusterIssuer name: cluster-authority --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: app-rollout-controller annotations: cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/app-rollout-controller webhooks: - name: apps.apps.cloudrobotics.com admissionReviewVersions: ["v1"] failurePolicy: Fail clientConfig: service: namespace: {{ .Release.Namespace }} name: app-rollout-controller path: /app/validate rules: - apiGroups: - apps.cloudrobotics.com apiVersions: - v1alpha1 operations: - CREATE - UPDATE resources: - apps sideEffects: None - name: approllouts.apps.cloudrobotics.com admissionReviewVersions: ["v1"] failurePolicy: Fail clientConfig: service: namespace: {{ .Release.Namespace }} name: app-rollout-controller path: /approllout/validate rules: - apiGroups: - apps.cloudrobotics.com apiVersions: - v1alpha1 operations: - CREATE - UPDATE resources: - approllouts sideEffects: None --- # chart-assignment-controller apiVersion: apps/v1 kind: Deployment metadata: name: chart-assignment-controller spec: replicas: 1 selector: matchLabels: app: chart-assignment-controller template: metadata: labels: app: chart-assignment-controller spec: serviceAccountName: chart-assignment-controller containers: - name: chart-assignment-controller image: {{ .Values.registry }}{{ .Values.images.chart_assignment_controller }} args: - "--cloud-cluster=true" - "--webhook-enabled=true" - "--webhook-port=9876" - "--cert-dir=/tls" env: - name: GOOGLE_CLOUD_PROJECT value: {{ .Values.project }} livenessProbe: initialDelaySeconds: 15 periodSeconds: 10 httpGet: port: 8080 path: /healthz ports: - name: webhook containerPort: 9876 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true volumeMounts: - name: tls mountPath: /tls - name: tmp mountPath: /tmp volumes: - name: tls secret: secretName: chart-assignment-controller-tls - name: tmp emptyDir: medium: Memory securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 --- # The chart assignment controller runs admission webhooks, which need to be served via TLS. apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: chart-assignment-controller spec: secretName: chart-assignment-controller-tls commonName: chart-assignment-controller.{{ .Release.Namespace }}.svc dnsNames: - chart-assignment-controller.{{ .Release.Namespace }}.svc - chart-assignment-controller.{{ .Release.Namespace }}.svc.cluster.local issuerRef: kind: ClusterIssuer name: cluster-authority --- apiVersion: v1 kind: Service metadata: name: chart-assignment-controller spec: type: ClusterIP ports: - port: 443 targetPort: webhook selector: app: chart-assignment-controller --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: chart-assignment-controller annotations: cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/chart-assignment-controller webhooks: - name: chartassignments.apps.cloudrobotics.com admissionReviewVersions: ["v1"] failurePolicy: Fail clientConfig: service: namespace: {{ .Release.Namespace }} name: chart-assignment-controller path: /chartassignment/validate rules: - apiGroups: - apps.cloudrobotics.com apiVersions: - v1alpha1 operations: - CREATE - UPDATE resources: - chartassignments sideEffects: None {{ end }} ================================================ FILE: src/app_charts/base/cloud/apps-crd.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: apps.apps.cloudrobotics.com annotations: helm.sh/resource-policy: keep spec: group: apps.cloudrobotics.com names: kind: App plural: apps singular: app scope: Cluster versions: - name: v1alpha1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: repository: type: string version: type: string components: type: object properties: cloud: type: object properties: name: type: string inline: type: string robot: type: object properties: name: type: string inline: type: string --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: approllouts.apps.cloudrobotics.com annotations: helm.sh/resource-policy: keep spec: group: apps.cloudrobotics.com names: kind: AppRollout plural: approllouts singular: approllout scope: Cluster versions: - name: v1alpha1 served: true storage: true subresources: status: {} additionalPrinterColumns: - jsonPath: .status.assignments name: Assignments type: integer - jsonPath: .status.readyAssignments name: Ready type: integer - jsonPath: .status.failedAssignments name: Failed type: integer - jsonPath: .metadata.creationTimestamp name: Age type: date schema: openAPIV3Schema: type: object properties: spec: type: object properties: appName: type: string cloud: type: object properties: values: type: object x-kubernetes-preserve-unknown-fields: true robots: type: array items: type: object properties: values: type: object x-kubernetes-preserve-unknown-fields: true version: type: string selector: type: object properties: any: type: boolean matchLabels: type: object x-kubernetes-preserve-unknown-fields: true matchExpressions: type: array items: type: object properties: key: type: string operator: type: string values: type: array items: type: string status: type: object properties: observedGeneration: type: integer assignments: type: integer readyAssignments: type: integer settledAssignments: type: integer failedAssignments: type: integer conditions: type: array items: type: object properties: lastUpdateTime: type: string format: date-time lastTransitionTime: type: string format: date-time status: type: string type: type: string message: type: string --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: chartassignments.apps.cloudrobotics.com annotations: cr-syncer.cloudrobotics.com/spec-source: cloud cr-syncer.cloudrobotics.com/filter-by-robot-name: "True" helm.sh/resource-policy: keep spec: group: apps.cloudrobotics.com names: kind: ChartAssignment plural: chartassignments singular: chartassignment scope: Cluster versions: - name: v1alpha1 served: true storage: true additionalPrinterColumns: - jsonPath: .status.phase name: Phase type: string - jsonPath: .status.observedGeneration name: Generation type: integer - jsonPath: .metadata.creationTimestamp name: Age type: date subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: clusterName: type: string namespaceName: type: string chart: type: object properties: repository: type: string name: type: string version: type: string inline: type: string values: type: object x-kubernetes-preserve-unknown-fields: true status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: lastUpdateTime: type: string format: date-time lastTransitionTime: type: string format: date-time status: type: string type: type: string message: type: string phase: type: string ================================================ FILE: src/app_charts/base/cloud/cert-ingress.yaml ================================================ # Owns the 'tls' block to ensure a cert is generated and avoids repetition of # the 'tls' block in other ingresses. Cert is associated via 'host' field. apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: certificate-ingress spec: ingressClassName: nginx tls: - hosts: - {{ .Values.domain }} rules: - {} ================================================ FILE: src/app_charts/base/cloud/cert-manager-certificates.yaml ================================================ apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true duration: 8760h # 1year. This needs to be at least 3x 90days commonName: {{ .Values.domain }} secretName: cluster-authority privateKey: algorithm: ECDSA size: 256 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io {{- if .Values.certificate_provider }} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: cloud-robotics spec: commonName: {{ .Values.domain }} secretName: tls dnsNames: - {{ .Values.domain }} {{- if eq .Values.certificate_provider "lets-encrypt" }} issuerRef: name: letsencrypt-prod kind: ClusterIssuer {{- else if eq .Values.certificate_provider "google-cas" }} issuerRef: name: google-cas group: cas-issuer.jetstack.io kind: GoogleCASClusterIssuer {{- end }} {{- end }} ================================================ FILE: src/app_charts/base/cloud/cert-manager-google-cas-issuer.yaml ================================================ {{- if eq .Values.certificate_provider "google-cas" }} # This includes all resources expanded from the cert-manager chart using # the values in ../cert-manager-cloud.values.yaml. # Some pseudo-variables that were inserted there are replaced with actual runtime values. {{ .Files.Get "files/cert-manager-google-cas-issuer-chart.cloud.yaml" | replace "HELM-NAMESPACE" .Release.Namespace | replace "PROJECT-ID" .Values.project }} {{- end }} ================================================ FILE: src/app_charts/base/cloud/cert-manager-issuers.yaml ================================================ # A self-signing issuer for cluster-internal services. apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: cluster-authority spec: ca: secretName: cluster-authority {{- if eq .Values.certificate_provider "lets-encrypt" }} --- # While an Issuer may satisfy our current needs within the default namespace, # anticipating future growth and potential deployments in additional namespaces, # adopting a ClusterIssuer offers a more scalable and versatile solution. apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: "{{ .Values.owner_email }}" privateKeySecretRef: name: letsencrypt-prod # We can't use dns01 since we don't control the dns-zone that endpoints uses. solvers: - http01: ingress: class: nginx {{- else if eq .Values.certificate_provider "google-cas" }} --- # Issuer for Google's Certificate Authority service (CAS) using the google-cas-issuer project. # https://github.com/jetstack/google-cas-issuer apiVersion: cas-issuer.jetstack.io/v1beta1 kind: GoogleCASClusterIssuer metadata: name: google-cas spec: project: {{ .Values.project }} location: {{ .Values.region }} caPoolId: "{{ .Values.project }}-ca-pool" {{- end }} ================================================ FILE: src/app_charts/base/cloud/cert-manager.yaml ================================================ # This includes all resources expanded from the cert-manager chart using # the values in ../cert-manager-cloud.values.yaml. # Some pseudo-variables that were inserted there are replaced with actual runtime values. {{ .Files.Get "files/cert-manager-chart.cloud.yaml" | replace "HELM-NAMESPACE" .Release.Namespace | replace "PROJECT-ID" .Values.project }} ================================================ FILE: src/app_charts/base/cloud/cr-syncer-auth-webhook.yaml ================================================ {{ if eq .Values.onprem_federation "true" }} # The cr-syncer-auth-webhook verifies that requests from the cr-syncer are # limited to the robot named in the credentials. apiVersion: apps/v1 kind: Deployment metadata: name: cr-syncer-auth-webhook spec: selector: matchLabels: app: cr-syncer-auth-webhook template: metadata: labels: app: cr-syncer-auth-webhook spec: containers: - name: cr-syncer-auth-webhook image: {{ .Values.registry }}{{ .Values.images.cr_syncer_auth_webhook }} args: - --port=8080 - --accept-legacy-service-account-credentials - --token-vendor=http://token-vendor.app-token-vendor.svc.cluster.local ports: - name: webhook containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 livenessProbe: httpGet: path: /healthz port: 8080 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 serviceAccountName: cr-syncer-auth-webhook --- # The incoming request from the cr-syncer will be extended with a header to # impersonate this SA if it passes the webhook's policy checks. apiVersion: v1 kind: ServiceAccount metadata: name: cr-syncer-auth-webhook --- apiVersion: v1 kind: Service metadata: name: cr-syncer-auth-webhook labels: app: cr-syncer-auth-webhook spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: cr-syncer-auth-webhook type: ClusterIP {{ end }} ================================================ FILE: src/app_charts/base/cloud/cr-syncer-policy.yaml ================================================ {{ if eq .Values.onprem_federation "true" }} # This policy lets the cr-syncer operate on the apps & registry CRDs. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:cr-syncer:base labels: cr-syncer.cloudrobotics.com/aggregate-to-robot-service: "true" rules: # To sync the specs from the cloud to the robot, the cr-syncer needs to read # the resources in the cloud cluster. Note that the Robot and ChartAssignment # CRDs enable the /status subresource, whereas the RobotType does not. - apiGroups: - registry.cloudrobotics.com resources: - robots - robots/status - robottypes verbs: - get - list - watch - apiGroups: - apps.cloudrobotics.com resources: - chartassignments - chartassignments/status verbs: - get - list - watch # Only the /status subresource can be updated. It's important that the robot # can update the status but not the spec, or it could run code on other robots. - apiGroups: - registry.cloudrobotics.com resources: - robots/status verbs: - update - apiGroups: - apps.cloudrobotics.com resources: - chartassignments/status verbs: - update --- # This aggregate role will combine all roles with the given label. This means # that policy can easily be added for CRDs beyond those listed above. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:cr-syncer aggregationRule: clusterRoleSelectors: - matchLabels: cr-syncer.cloudrobotics.com/aggregate-to-robot-service: "true" rules: [] # The control plane automatically fills in the rules --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cloud-robotics:cr-syncer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cloud-robotics:cr-syncer subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: robot-service@{{ .Values.project }}.iam.gserviceaccount.com # The grant for the cr-syncer-auth-webhook replaces the grant for the # robot-service@ account. - namespace: {{ .Release.Namespace }} kind: ServiceAccount name: cr-syncer-auth-webhook {{ end }} ================================================ FILE: src/app_charts/base/cloud/domain-redirect.yaml ================================================ {{ $endpointsURL := print "www.endpoints." .Values.project ".cloud.goog" }} {{ if ne $endpointsURL .Values.domain }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: domain-redirect annotations: nginx.ingress.kubernetes.io/permanent-redirect: http://{{ .Values.domain }} spec: ingressClassName: nginx rules: - host: "www.endpoints.{{ .Values.project }}.cloud.goog" http: paths: - path: / pathType: Prefix backend: service: # This isn't used, but needs to be specified. name: dummy port: number: 80 {{ end }} ================================================ FILE: src/app_charts/base/cloud/fluentd-metrics.yaml ================================================ # Adds a Prometheus ServiceMonitor for scraping the fluentd metrics. # By default, google-fluentd exports some Prometheus metrics on port 24231. apiVersion: v1 kind: Service metadata: name: fluentd-metrics labels: app: fluentd-metrics namespace: kube-system spec: ports: - port: 24231 name: metrics selector: k8s-app: fluentd-gcp type: ClusterIP --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: fluentd-metrics labels: prometheus: kube-prometheus namespace: kube-system spec: endpoints: - port: metrics path: /metrics interval: 10s relabelings: - sourceLabels: [__meta_kubernetes_pod_node_name] targetLabel: instance selector: matchLabels: app: fluentd-metrics ================================================ FILE: src/app_charts/base/cloud/kubernetes-api.yaml ================================================ {{ if eq .Values.onprem_federation "true" }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: kubernetes-api annotations: nginx.ingress.kubernetes.io/auth-url: http://cr-syncer-auth-webhook.default.svc.cluster.local/auth nginx.ingress.kubernetes.io/auth-response-headers: Authorization nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/backend-protocol: HTTPS nginx.ingress.kubernetes.io/proxy-read-timeout: "600" # seconds nginx.ingress.kubernetes.io/client-body-buffer-size: "50m" spec: ingressClassName: nginx rules: - host: {{ .Values.domain }} http: paths: - path: /apis/core.kubernetes($|/)(.*) pathType: Prefix backend: service: name: kubernetes port: number: 443 {{ end }} ================================================ FILE: src/app_charts/base/cloud/namespace.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: {{ .Release.Namespace }} labels: certmanager.k8s.io/disable-validation: "true" ================================================ FILE: src/app_charts/base/cloud/nginx-ingress-controller-policy.yaml ================================================ # Source: ingress-nginx/templates/controller-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: ingress-nginx automountServiceAccountToken: true --- # Source: ingress-nginx/templates/clusterrole.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: ingress-nginx rules: - apiGroups: - '' resources: - configmaps - endpoints - nodes - pods - secrets verbs: - list - watch - apiGroups: - '' resources: - nodes verbs: - get - apiGroups: - '' resources: - services verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses/status verbs: - update - apiGroups: - networking.k8s.io resources: - ingressclasses verbs: - get - list - watch - apiGroups: - coordination.k8s.io resourceNames: - ingress-controller-leader resources: - leases verbs: - get - update - apiGroups: - coordination.k8s.io resources: - leases verbs: - create - apiGroups: - '' resources: - events verbs: - create - patch - apiGroups: - discovery.k8s.io resources: - endpointslices verbs: - list - watch - get --- # Source: ingress-nginx/templates/clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: ingress-nginx subjects: - kind: ServiceAccount name: ingress-nginx namespace: {{ .Release.Namespace }} --- # Source: ingress-nginx/templates/controller-role.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: ingress-nginx rules: - apiGroups: - '' resources: - namespaces verbs: - get - apiGroups: - '' resources: - configmaps - pods - secrets - endpoints verbs: - get - list - watch - apiGroups: - coordination.k8s.io resources: - leases verbs: - list - watch - apiGroups: - '' resources: - services verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses/status verbs: - update - apiGroups: - networking.k8s.io resources: - ingressclasses verbs: - get - list - watch - apiGroups: - discovery.k8s.io resources: - endpointslices verbs: - list - watch - get - apiGroups: - '' resources: - configmaps resourceNames: - ingress-controller-leader verbs: - get - update - apiGroups: - '' resources: - configmaps verbs: - create - apiGroups: - '' resources: - events verbs: - create - patch --- # Source: ingress-nginx/templates/controller-rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: ingress-nginx subjects: - kind: ServiceAccount name: ingress-nginx namespace: {{ .Release.Namespace }} ================================================ FILE: src/app_charts/base/cloud/nginx-ingress-controller.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: nginx-ingress-controller data: # This is the same as the default but with the addition of $http_x_forwarded_for, # which is useful when the GKE Global Application LB is also pointed at nginx. # https://cloud.google.com/load-balancing/docs/https#x-forwarded-for_header log-format-upstream: $remote_addr - $remote_user - $http_x_forwarded_for [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_length $request_time [$proxy_upstream_name] [$proxy_alternative_upstream_name] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status $req_id # The token-vendor checks the Original-URI header to accept tokens from query # parameters. proxy-add-original-uri-header: "true" --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-ingress-controller labels: k8s-app: nginx-ingress-controller spec: selector: matchLabels: k8s-app: nginx-ingress-controller template: metadata: labels: k8s-app: nginx-ingress-controller spec: serviceAccountName: ingress-nginx dnsPolicy: ClusterFirst containers: - name: nginx-ingress-controller image: registry.k8s.io/ingress-nginx/controller-chroot:v1.8.4@sha256:76100ab4c1b3cdc2697dd26492ba42c6519e99c5df1bc839ac5d6444a2c58d17 lifecycle: preStop: exec: command: - /wait-shutdown resources: requests: memory: "1Gi" cpu: 1 args: - /nginx-ingress-controller - --v=1 - --default-backend-service=kube-system/default-http-backend - --publish-service=$(POD_NAMESPACE)/nginx-ingress-lb - --election-id=ingress-controller-leader - --ingress-class=nginx - --configmap=$(POD_NAMESPACE)/nginx-ingress-controller - --default-ssl-certificate=default/tls securityContext: capabilities: drop: - ALL add: - NET_BIND_SERVICE - SYS_CHROOT runAsUser: 101 allowPrivilegeEscalation: true env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: LD_PRELOAD value: /usr/local/lib/libmimalloc.so livenessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 5 readinessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3 ports: - name: http containerPort: 80 - name: https containerPort: 443 - name: healthz containerPort: 10254 nodeSelector: kubernetes.io/os: linux terminationGracePeriodSeconds: 300 --- apiVersion: v1 kind: Service metadata: name: nginx-ingress-lb labels: app: nginx-ingress-lb spec: type: LoadBalancer externalTrafficPolicy: Local loadBalancerIP: {{ .Values.ingress_ip }} ports: - port: 80 name: http targetPort: 80 appProtocol: HTTP - port: 443 name: https targetPort: 443 appProtocol: HTTPS selector: k8s-app: nginx-ingress-controller --- apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: labels: app.kubernetes.io/component: controller name: nginx annotations: ingressclass.kubernetes.io/is-default-class: "true" spec: controller: k8s.io/ingress-nginx --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx-ingress-controller spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-ingress-controller minReplicas: 1 maxReplicas: 5 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-ingress-controller-metrics labels: app: nginx-ingress-controller-metrics spec: ports: - port: 10254 name: healthz selector: k8s-app: nginx-ingress-controller type: ClusterIP --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: nginx-ingress-controller-metrics labels: prometheus: kube-prometheus spec: endpoints: - port: healthz relabelings: - sourceLabels: [__meta_kubernetes_pod_node_name] targetLabel: instance selector: matchLabels: app: nginx-ingress-controller-metrics --- apiVersion: v1 kind: ConfigMap metadata: name: nginx-dashboards-json labels: grafana: "1" data: nginx.json: |- {{ .Files.Get "files/nginx.json" | indent 4 }} request-handling-performance.json: |- {{ .Files.Get "files/request-handling-performance.json" | indent 4 }} ================================================ FILE: src/app_charts/base/cloud/oauth2-proxy.yaml ================================================ {{ if and (ne .Values.oauth2_proxy.client_id "") (ne .Values.oauth2_proxy.client_secret "") }} apiVersion: apps/v1 kind: Deployment metadata: name: oauth2-proxy spec: replicas: 1 selector: matchLabels: app: oauth2-proxy template: metadata: labels: app: oauth2-proxy spec: containers: - name: oauth2-proxy args: - --provider=oidc - --oidc-issuer-url=https://accounts.google.com - --email-domain=* - --upstream=http://token-vendor.default.svc.cluster.local/apis/core.token-vendor/ - --upstream=https://{{ .Values.domain }}/ - --http-address=0.0.0.0:8080 - --pass-access-token - --pass-host-header - "--scope=profile email https://www.googleapis.com/auth/iam" - --cookie-expire=168h - --cookie-refresh=1h env: - name: OAUTH2_PROXY_CLIENT_ID value: {{ .Values.oauth2_proxy.client_id }} - name: OAUTH2_PROXY_CLIENT_SECRET value: {{ .Values.oauth2_proxy.client_secret }} - name: OAUTH2_PROXY_COOKIE_SECRET value: {{ .Values.oauth2_proxy.cookie_secret }} image: quay.io/oauth2-proxy/oauth2-proxy:v7.5.1 ports: - name: http containerPort: 8080 protocol: TCP securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 --- apiVersion: v1 kind: Service metadata: name: oauth2-proxy spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: oauth2-proxy type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: oauth2-proxy annotations: nginx.ingress.kubernetes.io/rewrite-target: /apis/$2 nginx.ingress.kubernetes.io/proxy-read-timeout: "600" # seconds spec: ingressClassName: nginx rules: - host: {{ .Values.domain }} http: paths: - path: /web-apis($|/)(.*) pathType: Prefix backend: service: name: oauth2-proxy port: name: http --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: oauth2-proxy-interactive spec: ingressClassName: nginx rules: - host: {{ .Values.domain }} http: paths: - path: "/oauth2" pathType: Prefix backend: service: name: oauth2-proxy port: name: http {{ end }} ================================================ FILE: src/app_charts/base/cloud/registry-crd.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: robottypes.registry.cloudrobotics.com annotations: cr-syncer.cloudrobotics.com/spec-source: cloud helm.sh/resource-policy: keep spec: group: registry.cloudrobotics.com names: kind: RobotType plural: robottypes singular: robottype scope: Namespaced versions: - name: v1alpha1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object required: ['make', 'model'] maxProperties: 2 properties: make: type: string model: type: string --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: robots.registry.cloudrobotics.com annotations: cr-syncer.cloudrobotics.com/filter-by-robot-name: "True" cr-syncer.cloudrobotics.com/status-subtree: "robot" cr-syncer.cloudrobotics.com/spec-source: cloud helm.sh/resource-policy: keep spec: group: registry.cloudrobotics.com names: kind: Robot plural: robots singular: robot scope: Namespaced versions: - name: v1alpha1 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object maxProperties: 3 properties: type: type: string project: type: string status: type: object properties: cloud: type: object x-kubernetes-preserve-unknown-fields: true robot: type: object properties: info: type: object additionalProperties: type: string updateTime: type: string state: type: string enum: - UNDEFINED - UNAVAILABLE - AVAILABLE - EMERGENCY_STOP - ERROR lastStateChangeTime: type: string batteryPercentage: type: number emergencyStopButtonPressed: type: boolean configuration: type: object properties: trolleyAttached: type: boolean ================================================ FILE: src/app_charts/base/cloud/registry-policy.yaml ================================================ # This policy lets the human-acl GCP SA register robots. For context, see the # IAM policy in service-account.tf. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cloud-robotics:robot-creator rules: - apiGroups: - registry.cloudrobotics.com resources: - robots - robots/status verbs: - create - get - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cloud-robotics:human-acl roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cloud-robotics:robot-creator subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: human-acl@{{ .Values.project }}.iam.gserviceaccount.com ================================================ FILE: src/app_charts/base/cloud/relay-dashboards.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: relay-dashboards-json labels: grafana: "1" data: relay-dashboard.json: |- {{ .Files.Get "files/relay-dashboard.json" | indent 4 }} ================================================ FILE: src/app_charts/base/cloud/token-vendor-app-fwd.yaml ================================================ # The Token Vendor was moved to the app namespace. # We create this service here in default namespace # as some codepaths still use the hard-coded # "token-vendor.default.svc.cluster.local" address. apiVersion: v1 kind: Service metadata: name: token-vendor annotations: spec: ports: - port: 80 targetPort: 9090 protocol: TCP name: token-vendor type: ExternalName externalName: token-vendor.app-token-vendor.svc.cluster.local ================================================ FILE: src/app_charts/base/cloud/token-vendor-rollout.yaml ================================================ apiVersion: apps.cloudrobotics.com/v1alpha1 kind: AppRollout metadata: name: token-vendor labels: app: token-vendor spec: appName: token-vendor-dev cloud: {} ================================================ FILE: src/app_charts/base/fluent-bit-helm.sh ================================================ #!/bin/bash # needs at least helm v3.5.0 OUTPUT=./robot/fluent-bit.yaml TEMPLATE_VERSION=0.48.9 helm repo add fluent https://fluent.github.io/helm-charts helm repo update fluent helm template fluent-bit fluent/fluent-bit --version ${TEMPLATE_VERSION} -f fluent-bit-values.yaml --skip-tests > ${OUTPUT} sed -i '1i\{{ if and (eq .Values.robot_authentication "true") (eq .Values.fluentbit "true") }}' ${OUTPUT} sed -i '1i\# !!! DO NOT EDIT THIS FILE !!!\n# This file is autogenerated using src/app_charts/base/fluent-bit-helm.sh.\n# See src/app_charts/base/README.md for update instructions.' ${OUTPUT} sed -i '$a\{{ end }}' ${OUTPUT} sed -i 's/MY_ROBOT/{{ .Values.robot.name }}/' ${OUTPUT} # Add a template expressions for prepending a subdomain to the fluentbit Tag_Prefix # If no subdomain is supplied, the Tag_Prefix will resolve to "kube.var.log.containers." # Otherwise, if subdomain is supplied (without the "." at the end), the Tag_Prefix will be ".kube.var.log.containers." sed -i 's/kube\.\*/{{ empty .Values.log_prefix_subdomain | ternary "" (print .Values.log_prefix_subdomain "." ) -}} kube.*/' ${OUTPUT} sed -i 's/Tag_Prefix kube.var.log.containers./Tag_Prefix {{ empty .Values.log_prefix_subdomain | ternary "" (print .Values.log_prefix_subdomain "." ) -}} kube.var.log.containers./' ${OUTPUT} # This needs to be an actual Cloud zone so that it can be mapped # to a Monarch/Stackdriver region. TODO(swolter): We should make # this zone configurable to avoid confusing users. sed -i 's/MY_CLUSTER_LOCATION/europe-west1-c/' ${OUTPUT} ================================================ FILE: src/app_charts/base/fluent-bit-values.yaml ================================================ image: pullPolicy: IfNotPresent env: - name: MY_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName ## https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/configuration-file config: service: | [SERVICE] Daemon Off Flush {{ .Values.flush }} Log_Level {{ .Values.logLevel }} Parsers_File custom_parsers.conf HTTP_Server On HTTP_Listen 0.0.0.0 HTTP_Port {{ .Values.metricsPort }} Health_Check On ## https://docs.fluentbit.io/manual/pipeline/parsers customParsers: | # Merges multi-line Abseil logs. The regexs assume that any line which does not start # with an Abseil log preamble is part of the previous log message. No assumptions on # indentation or similar are made. [MULTILINE_PARSER] Name absl_logs_multiline Type regex Flush_timeout 1000 # # Regex rules for multiline parsing # --------------------------------- # # configuration hints: # # - first state always has the name: start_state # - every field in the rule must be inside double quotes # # rules | state name | regex pattern | next state # ------|---------------|--------------------------------------------------------------------------------------------- Rule "start_state" "/^((W|I|E|F))([0-9]{4}) ([^ ]+)\s+([-0-9]+) (\S+:\d+)] (.*)$/" "cont" Rule "cont" "/^(?!(((W|I|E|F))([0-9]{4}) ([^ ]+)\s+([-0-9]+) (\S+:\d+)]))(.*)$/" "cont" # A parser for Abseil log files: https://abseil.io/docs/cpp/guides/logging#prefix [PARSER] Name absl_logs Format regex Regex ^(?(W|I|E|F))([0-9]{4}) (?