From d58cfc46fd6d37b8fcfe033c1bb04bfd20c3fa8e Mon Sep 17 00:00:00 2001 From: Matt Butcher <mbutcher@engineyard.com> Date: Wed, 27 Apr 2016 21:29:54 -0600 Subject: [PATCH] reset --- .gitignore | 19 - CONTRIBUTING.md | 81 -- LICENSE | 202 ----- MAINTAINERS.md | 61 -- Makefile | 118 --- README.md | 162 ---- circle.yml | 33 - cmd/doc.go | 18 - cmd/expandybird/expander/expander.go | 131 --- cmd/expandybird/expander/expander_test.go | 832 ------------------ cmd/expandybird/main.go | 45 - cmd/expandybird/test/ExpectedOutput.yaml | 77 -- cmd/expandybird/test/InvalidFileName.yaml | 22 - cmd/expandybird/test/InvalidProperty.yaml | 22 - cmd/expandybird/test/InvalidTypeName.yaml | 22 - cmd/expandybird/test/MalformedContent.yaml | 20 - cmd/expandybird/test/MissingImports.yaml | 21 - cmd/expandybird/test/MissingResourceName.yaml | 21 - cmd/expandybird/test/MissingTypeName.yaml | 21 - cmd/expandybird/test/TestArchive.tar | Bin 9732 -> 0 bytes cmd/expandybird/test/ValidContent.yaml | 27 - cmd/expandybird/test/replicatedservice.py | 182 ---- cmd/expandybird/test/schemas/bad.jinja.schema | 9 - .../test/schemas/default_ref.jinja.schema | 14 - .../test/schemas/defaults.jinja.schema | 12 - .../test/schemas/defaults.py.schema | 12 - .../test/schemas/invalid_default.jinja.schema | 11 - .../test/schemas/invalid_reference.py.schema | 10 - .../invalid_reference_schema.py.schema | 8 - .../test/schemas/metadata.py.schema | 20 - .../test/schemas/missing_quote.py.schema | 11 - .../test/schemas/nested_defaults.py.schema | 33 - .../test/schemas/numbers.py.schema | 27 - .../schemas/ref_nested_defaults.py.schema | 36 - .../test/schemas/reference.jinja.schema | 14 - .../test/schemas/req_default_ref.py.schema | 14 - .../test/schemas/required.jinja.schema | 10 - .../schemas/required_default.jinja.schema | 11 - .../test/templates/description_text.txt | 1 - .../test/templates/duplicate_names.yaml | 9 - .../test/templates/duplicate_names_B.jinja | 5 - .../test/templates/duplicate_names_C.jinja | 5 - .../duplicate_names_in_subtemplates.jinja | 9 - .../duplicate_names_in_subtemplates.yaml | 5 - .../duplicate_names_mixed_level.yaml | 7 - .../duplicate_names_mixed_level_result.yaml | 22 - .../duplicate_names_parent_child.yaml | 7 - .../duplicate_names_parent_child_result.yaml | 17 - cmd/expandybird/test/templates/helper.jinja | 5 - .../test/templates/helper.jinja.schema | 4 - .../test/templates/helpers/common.jinja | 3 - .../test/templates/helpers/common.py | 6 - .../test/templates/helpers/extra/__init__.py | 1 - .../test/templates/helpers/extra/common2.py | 6 - .../test/templates/invalid_config.yaml | 2 - .../test/templates/jinja_defaults.jinja | 16 - .../templates/jinja_defaults.jinja.schema | 18 - .../test/templates/jinja_defaults.yaml | 9 - .../test/templates/jinja_defaults_result.yaml | 29 - .../templates/jinja_missing_required.jinja | 4 - .../jinja_missing_required.jinja.schema | 11 - .../templates/jinja_missing_required.yaml | 9 - .../templates/jinja_multiple_errors.jinja | 4 - .../jinja_multiple_errors.jinja.schema | 22 - .../test/templates/jinja_multiple_errors.yaml | 12 - .../test/templates/jinja_noparams.jinja | 19 - .../test/templates/jinja_noparams.yaml | 6 - .../test/templates/jinja_noparams_result.yaml | 41 - .../test/templates/jinja_template.jinja | 18 - .../test/templates/jinja_template.yaml | 10 - .../test/templates/jinja_template_result.yaml | 28 - .../templates/jinja_template_with_env.jinja | 18 - .../templates/jinja_template_with_env.yaml | 8 - .../jinja_template_with_env_result.yaml | 26 - .../jinja_template_with_import.jinja | 6 - .../templates/jinja_template_with_import.yaml | 5 - .../jinja_template_with_import_result.yaml | 13 - .../jinja_template_with_inlinedfile.jinja | 7 - .../jinja_template_with_inlinedfile.yaml | 7 - ...inja_template_with_inlinedfile_result.yaml | 21 - .../test/templates/jinja_unresolved.jinja | 18 - .../test/templates/jinja_unresolved.yaml | 10 - .../test/templates/no_properties.py | 5 - .../test/templates/no_properties.yaml | 6 - .../test/templates/no_properties_result.yaml | 6 - .../test/templates/no_resources.py | 6 - .../test/templates/no_resources.yaml | 6 - .../templates/python_and_jinja_template.jinja | 17 - .../templates/python_and_jinja_template.py | 35 - .../templates/python_and_jinja_template.yaml | 9 - .../python_and_jinja_template_result.yaml | 35 - .../test/templates/python_bad_schema.py | 3 - .../templates/python_bad_schema.py.schema | 19 - .../test/templates/python_bad_schema.yaml | 9 - .../test/templates/python_noparams.py | 12 - .../test/templates/python_noparams.yaml | 9 - .../templates/python_noparams_result.yaml | 19 - .../test/templates/python_schema.py | 57 -- .../test/templates/python_schema.py.schema | 14 - .../test/templates/python_schema.yaml | 10 - .../test/templates/python_schema_result.yaml | 46 - .../test/templates/python_template.py | 57 -- .../test/templates/python_template.yaml | 9 - .../templates/python_template_result.yaml | 47 - .../templates/python_template_with_env.py | 57 -- .../templates/python_template_with_env.yaml | 8 - .../python_template_with_env_result.yaml | 46 - .../templates/python_template_with_import.py | 18 - .../python_template_with_import.yaml | 5 - .../python_template_with_import_result.yaml | 13 - .../python_template_with_inlinedfile.py | 20 - .../python_template_with_inlinedfile.yaml | 7 - ...thon_template_with_inlinedfile_result.yaml | 21 - .../test/templates/python_with_exception.py | 7 - .../test/templates/python_with_exception.yaml | 9 - cmd/expandybird/test/templates/simple.yaml | 18 - .../test/templates/simple_result.yaml | 21 - .../test/templates/use_helper.jinja | 7 - .../test/templates/use_helper.jinja.schema | 4 - .../test/templates/use_helper.yaml | 3 - .../test/templates/use_helper_result.yaml | 26 - cmd/goexpander/expander/expander.go | 146 --- cmd/goexpander/expander/expander_test.go | 262 ------ cmd/goexpander/main.go | 41 - cmd/helm/Makefile | 39 - cmd/helm/chart.go | 113 --- cmd/helm/chart_upload.go | 115 --- cmd/helm/deploy.go | 111 --- cmd/helm/deployment.go | 187 ---- cmd/helm/deployment_test.go | 107 --- cmd/helm/doctor.go | 49 -- cmd/helm/helm.go | 119 --- cmd/helm/helm_test.go | 118 --- cmd/helm/properties.go | 56 -- cmd/helm/repository.go | 130 --- cmd/helm/server.go | 195 ---- cmd/manager/chartrepos.go | 169 ---- cmd/manager/chartrepos_test.go | 174 ---- cmd/manager/deployments.go | 425 --------- cmd/manager/deployments_test.go | 215 ----- cmd/manager/main.go | 88 -- cmd/manager/manager/deployer.go | 187 ---- cmd/manager/manager/deployer_test.go | 304 ------- cmd/manager/manager/expander.go | 225 ----- cmd/manager/manager/expander_test.go | 373 -------- cmd/manager/manager/manager.go | 447 ---------- cmd/manager/manager/manager_test.go | 551 ------------ .../repository/persistent/persistent.go | 488 ---------- .../repository/persistent/persistent_test.go | 110 --- cmd/manager/repository/repository.go | 49 -- cmd/manager/repository/test_common.go | 340 ------- cmd/manager/repository/transient/transient.go | 325 ------- .../repository/transient/transient_test.go | 55 -- cmd/manager/router/context.go | 69 -- cmd/manager/router/router.go | 96 -- cmd/manager/router/router_test.go | 56 -- cmd/manager/testutil.go | 172 ---- cmd/resourcifier/configurations.go | 255 ------ cmd/resourcifier/configurator/configurator.go | 262 ------ cmd/resourcifier/main.go | 84 -- docs/design/architecture.dia | Bin 1785 -> 0 bytes docs/design/architecture.png | Bin 13472 -> 0 bytes docs/design/chart_format.md | 363 -------- docs/design/design.md | 434 --------- docs/design/provenance_proposal.md | 67 -- docs/design/user_stories.md | 33 - docs/pushing.md | 22 - docs/pushing_charts.md | 64 -- docs/templates/registry.md | 289 ------ docs/test-architecture.md | 59 -- docs/usage_docs/authoring_charts.md | 1 - docs/usage_docs/getting-started-guide.md | 22 - docs/workflow/developer-workflows.md | 540 ------------ docs/workflow/helm-dm-diagrams.src.md | 84 -- docs/workflow/helm-official-workflow.png | Bin 30322 -> 0 bytes docs/workflow/private-chart-no-repo.png | Bin 18297 -> 0 bytes docs/workflow/private-chart-repo.png | Bin 45502 -> 0 bytes docs/workflow/public-chart-repo.png | Bin 22859 -> 0 bytes docs/workflow/team-workflows.md | 226 ----- examples/charts/nginx/Chart.yaml | 12 - examples/charts/nginx/LICENSE | 0 examples/charts/nginx/README.md | 1 - examples/charts/nginx/templates/nginx-rc.yaml | 18 - .../charts/nginx/templates/nginx-svc.yaml | 13 - examples/charts/redis/Chart.yaml | 7 - examples/charts/redis/templates/redis.jinja | 32 - .../charts/redis/templates/redis.jinja.schema | 10 - examples/charts/replicatedservice/Chart.yaml | 7 - .../templates/replicatedservice.py | 195 ---- .../templates/replicatedservice.py.schema | 91 -- examples/guestbook/README.md | 125 --- examples/guestbook/guestbook.yaml | 12 - examples/package/cassandra.yaml | 4 - examples/wordpress/README.md | 155 ---- examples/wordpress/architecture.png | Bin 33703 -> 0 bytes examples/wordpress/images/nginx/Dockerfile | 2 - examples/wordpress/images/nginx/Makefile | 23 - examples/wordpress/images/nginx/default.conf | 48 - examples/wordpress/wordpress-resources.yaml | 12 - examples/wordpress/wordpress.jinja | 71 -- examples/wordpress/wordpress.jinja.schema | 69 -- examples/wordpress/wordpress.yaml | 6 - expansion/expansion.py | 394 --------- expansion/expansion_test.py | 511 ----------- expansion/file_expander.py | 49 -- expansion/requirements.txt | 3 - expansion/sandbox_loader.py | 88 -- expansion/schema_validation.py | 240 ----- expansion/schema_validation_test.py | 619 ------------- expansion/schema_validation_utils.py | 99 --- get-install.sh | 60 -- glide.lock | 70 -- glide.yaml | 24 - hack/README.md | 24 - hack/Vagrantfile | 49 -- pkg/chart/chart.go | 460 ---------- pkg/chart/chart_test.go | 287 ------ pkg/chart/chartfile.go | 110 --- pkg/chart/chartfile_test.go | 91 -- pkg/chart/doc.go | 23 - pkg/chart/locator.go | 234 ----- pkg/chart/locator_test.go | 210 ----- pkg/chart/save.go | 120 --- pkg/chart/save_test.go | 123 --- pkg/chart/testdata/README.md | 9 - pkg/chart/testdata/frobnitz-0.0.1.tgz | Bin 2382 -> 0 bytes pkg/chart/testdata/frobnitz/Chart.yaml | 33 - pkg/chart/testdata/frobnitz/LICENSE | 1 - pkg/chart/testdata/frobnitz/README.md | 11 - pkg/chart/testdata/frobnitz/docs/README.md | 1 - .../testdata/frobnitz/hooks/pre-install.py | 1 - pkg/chart/testdata/frobnitz/icon.svg | 8 - .../templates/wordpress-resources.yaml | 12 - .../frobnitz/templates/wordpress.jinja | 72 -- .../frobnitz/templates/wordpress.jinja.schema | 69 -- .../frobnitz/templates/wordpress.yaml | 6 - pkg/chart/testdata/ill-1.2.3.tgz | Bin 2305 -> 0 bytes pkg/chart/testdata/ill/Chart.yaml | 28 - pkg/chart/testdata/ill/LICENSE | 1 - pkg/chart/testdata/ill/README.md | 11 - pkg/chart/testdata/ill/docs/README.md | 1 - pkg/chart/testdata/ill/hooks/pre-install.py | 1 - .../ill/templates/wordpress-resources.yaml | 12 - .../testdata/ill/templates/wordpress.jinja | 72 -- .../ill/templates/wordpress.jinja.schema | 69 -- .../testdata/ill/templates/wordpress.yaml | 6 - pkg/chart/testdata/nochart.tgz | Bin 325 -> 0 bytes pkg/chart/testdata/sprocket/Chart.yaml | 4 - pkg/chart/testdata/sprocket/LICENSE | 1 - pkg/chart/testdata/sprocket/README.md | 3 - pkg/chart/testdata/sprocket/docs/README.md | 1 - .../testdata/sprocket/hooks/pre-install.py | 1 - pkg/chart/testdata/sprocket/icon.svg | 8 - .../sprocket/templates/placeholder.txt | 1 - pkg/client/client.go | 282 ------ pkg/client/client_test.go | 107 --- pkg/client/deployments.go | 136 --- pkg/client/deployments_test.go | 163 ---- pkg/client/install.go | 237 ----- pkg/client/transport.go | 82 -- pkg/client/transport_test.go | 65 -- pkg/client/uninstall.go | 30 - pkg/common/types.go | 155 ---- pkg/doc.go | 2 - pkg/expansion/service.go | 96 -- pkg/expansion/service_test.go | 275 ------ pkg/expansion/types.go | 38 - pkg/expansion/validate.go | 126 --- pkg/expansion/validate_test.go | 194 ---- pkg/format/messages.go | 102 --- pkg/format/messages_test.go | 36 - pkg/httputil/doc.go | 21 - pkg/httputil/encoder.go | 194 ---- pkg/httputil/encoder_test.go | 162 ---- pkg/httputil/httperrors.go | 81 -- pkg/httputil/httperrors_test.go | 50 -- pkg/kubectl/cluster_info.go | 28 - pkg/kubectl/command.go | 49 -- pkg/kubectl/create.go | 37 - pkg/kubectl/create_test.go | 38 - pkg/kubectl/delete.go | 34 - pkg/kubectl/get.go | 74 -- pkg/kubectl/get_test.go | 45 - pkg/kubectl/kubectl.go | 48 - pkg/kubectl/kubectl_test.go | 32 - pkg/log/log.go | 68 -- pkg/log/log_test.go | 65 -- pkg/pkg_test.go | 15 - pkg/repo/filebased_credential_provider.go | 82 -- .../filebased_credential_provider_test.go | 57 -- pkg/repo/gcs_repo.go | 191 ---- pkg/repo/gcs_repo_test.go | 138 --- pkg/repo/inmem_credential_provider.go | 54 -- pkg/repo/inmem_credential_provider_test.go | 72 -- pkg/repo/inmem_repo_service.go | 156 ---- pkg/repo/inmem_repo_service_test.go | 145 --- pkg/repo/repo.go | 115 --- pkg/repo/repo_test.go | 67 -- pkg/repo/repoprovider.go | 258 ------ pkg/repo/repoprovider_test.go | 171 ---- pkg/repo/secrets_credential_provider.go | 133 --- pkg/repo/testdata/test_credentials_file.yaml | 6 - pkg/repo/types.go | 144 --- pkg/util/httpclient.go | 162 ---- pkg/util/httpclient_test.go | 163 ---- pkg/util/httputil.go | 251 ------ pkg/util/kubernetes.go | 119 --- pkg/util/kubernetes_kubectl.go | 147 ---- pkg/util/kubernetesutil.go | 54 -- pkg/util/kubernetesutil_test.go | 167 ---- pkg/util/templateutil.go | 196 ----- pkg/util/templateutil_test.go | 148 ---- pkg/version/version.go | 26 - rootfs/Makefile | 26 - rootfs/README.md | 26 - rootfs/expandybird/.dockerignore | 3 - rootfs/expandybird/Dockerfile | 27 - rootfs/expandybird/Makefile | 24 - rootfs/include.mk | 88 -- rootfs/manager/.dockerignore | 3 - rootfs/manager/Dockerfile | 22 - rootfs/manager/Makefile | 21 - rootfs/resourcifier/.dockerignore | 3 - rootfs/resourcifier/Dockerfile | 22 - rootfs/resourcifier/Makefile | 21 - scripts/build-go.sh | 28 - scripts/cluster/kube-system.yaml | 4 - scripts/cluster/skydns.yaml | 137 --- scripts/common.sh | 114 --- scripts/coverage.sh | 38 - scripts/docker.sh | 54 -- scripts/kube-down.sh | 74 -- scripts/kube-up.sh | 195 ---- scripts/kubectl.sh | 30 - scripts/start-local.sh | 69 -- scripts/stop-local.sh | 13 - scripts/validate-go.sh | 54 -- 337 files changed, 25864 deletions(-) delete mode 100644 .gitignore delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 MAINTAINERS.md delete mode 100644 Makefile delete mode 100644 README.md delete mode 100644 circle.yml delete mode 100644 cmd/doc.go delete mode 100644 cmd/expandybird/expander/expander.go delete mode 100644 cmd/expandybird/expander/expander_test.go delete mode 100644 cmd/expandybird/main.go delete mode 100644 cmd/expandybird/test/ExpectedOutput.yaml delete mode 100644 cmd/expandybird/test/InvalidFileName.yaml delete mode 100644 cmd/expandybird/test/InvalidProperty.yaml delete mode 100644 cmd/expandybird/test/InvalidTypeName.yaml delete mode 100644 cmd/expandybird/test/MalformedContent.yaml delete mode 100644 cmd/expandybird/test/MissingImports.yaml delete mode 100644 cmd/expandybird/test/MissingResourceName.yaml delete mode 100644 cmd/expandybird/test/MissingTypeName.yaml delete mode 100644 cmd/expandybird/test/TestArchive.tar delete mode 100644 cmd/expandybird/test/ValidContent.yaml delete mode 100644 cmd/expandybird/test/replicatedservice.py delete mode 100644 cmd/expandybird/test/schemas/bad.jinja.schema delete mode 100644 cmd/expandybird/test/schemas/default_ref.jinja.schema delete mode 100644 cmd/expandybird/test/schemas/defaults.jinja.schema delete mode 100644 cmd/expandybird/test/schemas/defaults.py.schema delete mode 100644 cmd/expandybird/test/schemas/invalid_default.jinja.schema delete mode 100644 cmd/expandybird/test/schemas/invalid_reference.py.schema delete mode 100644 cmd/expandybird/test/schemas/invalid_reference_schema.py.schema delete mode 100644 cmd/expandybird/test/schemas/metadata.py.schema delete mode 100644 cmd/expandybird/test/schemas/missing_quote.py.schema delete mode 100644 cmd/expandybird/test/schemas/nested_defaults.py.schema delete mode 100644 cmd/expandybird/test/schemas/numbers.py.schema delete mode 100644 cmd/expandybird/test/schemas/ref_nested_defaults.py.schema delete mode 100644 cmd/expandybird/test/schemas/reference.jinja.schema delete mode 100644 cmd/expandybird/test/schemas/req_default_ref.py.schema delete mode 100644 cmd/expandybird/test/schemas/required.jinja.schema delete mode 100644 cmd/expandybird/test/schemas/required_default.jinja.schema delete mode 100644 cmd/expandybird/test/templates/description_text.txt delete mode 100644 cmd/expandybird/test/templates/duplicate_names.yaml delete mode 100644 cmd/expandybird/test/templates/duplicate_names_B.jinja delete mode 100644 cmd/expandybird/test/templates/duplicate_names_C.jinja delete mode 100644 cmd/expandybird/test/templates/duplicate_names_in_subtemplates.jinja delete mode 100644 cmd/expandybird/test/templates/duplicate_names_in_subtemplates.yaml delete mode 100644 cmd/expandybird/test/templates/duplicate_names_mixed_level.yaml delete mode 100644 cmd/expandybird/test/templates/duplicate_names_mixed_level_result.yaml delete mode 100644 cmd/expandybird/test/templates/duplicate_names_parent_child.yaml delete mode 100644 cmd/expandybird/test/templates/duplicate_names_parent_child_result.yaml delete mode 100644 cmd/expandybird/test/templates/helper.jinja delete mode 100644 cmd/expandybird/test/templates/helper.jinja.schema delete mode 100644 cmd/expandybird/test/templates/helpers/common.jinja delete mode 100644 cmd/expandybird/test/templates/helpers/common.py delete mode 100644 cmd/expandybird/test/templates/helpers/extra/__init__.py delete mode 100644 cmd/expandybird/test/templates/helpers/extra/common2.py delete mode 100644 cmd/expandybird/test/templates/invalid_config.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_defaults.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_defaults.jinja.schema delete mode 100644 cmd/expandybird/test/templates/jinja_defaults.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_defaults_result.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_missing_required.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_missing_required.jinja.schema delete mode 100644 cmd/expandybird/test/templates/jinja_missing_required.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_multiple_errors.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_multiple_errors.jinja.schema delete mode 100644 cmd/expandybird/test/templates/jinja_multiple_errors.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_noparams.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_noparams.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_noparams_result.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_template.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_result.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_env.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_env.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_env_result.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_import.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_import.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_import_result.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_inlinedfile.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_inlinedfile.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_template_with_inlinedfile_result.yaml delete mode 100644 cmd/expandybird/test/templates/jinja_unresolved.jinja delete mode 100644 cmd/expandybird/test/templates/jinja_unresolved.yaml delete mode 100644 cmd/expandybird/test/templates/no_properties.py delete mode 100644 cmd/expandybird/test/templates/no_properties.yaml delete mode 100644 cmd/expandybird/test/templates/no_properties_result.yaml delete mode 100644 cmd/expandybird/test/templates/no_resources.py delete mode 100644 cmd/expandybird/test/templates/no_resources.yaml delete mode 100644 cmd/expandybird/test/templates/python_and_jinja_template.jinja delete mode 100644 cmd/expandybird/test/templates/python_and_jinja_template.py delete mode 100644 cmd/expandybird/test/templates/python_and_jinja_template.yaml delete mode 100644 cmd/expandybird/test/templates/python_and_jinja_template_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_bad_schema.py delete mode 100644 cmd/expandybird/test/templates/python_bad_schema.py.schema delete mode 100644 cmd/expandybird/test/templates/python_bad_schema.yaml delete mode 100644 cmd/expandybird/test/templates/python_noparams.py delete mode 100644 cmd/expandybird/test/templates/python_noparams.yaml delete mode 100644 cmd/expandybird/test/templates/python_noparams_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_schema.py delete mode 100644 cmd/expandybird/test/templates/python_schema.py.schema delete mode 100644 cmd/expandybird/test/templates/python_schema.yaml delete mode 100644 cmd/expandybird/test/templates/python_schema_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_template.py delete mode 100644 cmd/expandybird/test/templates/python_template.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_with_env.py delete mode 100644 cmd/expandybird/test/templates/python_template_with_env.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_with_env_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_with_import.py delete mode 100644 cmd/expandybird/test/templates/python_template_with_import.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_with_import_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_with_inlinedfile.py delete mode 100644 cmd/expandybird/test/templates/python_template_with_inlinedfile.yaml delete mode 100644 cmd/expandybird/test/templates/python_template_with_inlinedfile_result.yaml delete mode 100644 cmd/expandybird/test/templates/python_with_exception.py delete mode 100644 cmd/expandybird/test/templates/python_with_exception.yaml delete mode 100644 cmd/expandybird/test/templates/simple.yaml delete mode 100644 cmd/expandybird/test/templates/simple_result.yaml delete mode 100644 cmd/expandybird/test/templates/use_helper.jinja delete mode 100644 cmd/expandybird/test/templates/use_helper.jinja.schema delete mode 100644 cmd/expandybird/test/templates/use_helper.yaml delete mode 100644 cmd/expandybird/test/templates/use_helper_result.yaml delete mode 100644 cmd/goexpander/expander/expander.go delete mode 100644 cmd/goexpander/expander/expander_test.go delete mode 100644 cmd/goexpander/main.go delete mode 100644 cmd/helm/Makefile delete mode 100644 cmd/helm/chart.go delete mode 100644 cmd/helm/chart_upload.go delete mode 100644 cmd/helm/deploy.go delete mode 100644 cmd/helm/deployment.go delete mode 100644 cmd/helm/deployment_test.go delete mode 100644 cmd/helm/doctor.go delete mode 100644 cmd/helm/helm.go delete mode 100644 cmd/helm/helm_test.go delete mode 100644 cmd/helm/properties.go delete mode 100644 cmd/helm/repository.go delete mode 100644 cmd/helm/server.go delete mode 100644 cmd/manager/chartrepos.go delete mode 100644 cmd/manager/chartrepos_test.go delete mode 100644 cmd/manager/deployments.go delete mode 100644 cmd/manager/deployments_test.go delete mode 100644 cmd/manager/main.go delete mode 100644 cmd/manager/manager/deployer.go delete mode 100644 cmd/manager/manager/deployer_test.go delete mode 100644 cmd/manager/manager/expander.go delete mode 100644 cmd/manager/manager/expander_test.go delete mode 100644 cmd/manager/manager/manager.go delete mode 100644 cmd/manager/manager/manager_test.go delete mode 100644 cmd/manager/repository/persistent/persistent.go delete mode 100644 cmd/manager/repository/persistent/persistent_test.go delete mode 100644 cmd/manager/repository/repository.go delete mode 100644 cmd/manager/repository/test_common.go delete mode 100644 cmd/manager/repository/transient/transient.go delete mode 100644 cmd/manager/repository/transient/transient_test.go delete mode 100644 cmd/manager/router/context.go delete mode 100644 cmd/manager/router/router.go delete mode 100644 cmd/manager/router/router_test.go delete mode 100644 cmd/manager/testutil.go delete mode 100644 cmd/resourcifier/configurations.go delete mode 100644 cmd/resourcifier/configurator/configurator.go delete mode 100644 cmd/resourcifier/main.go delete mode 100644 docs/design/architecture.dia delete mode 100644 docs/design/architecture.png delete mode 100644 docs/design/chart_format.md delete mode 100644 docs/design/design.md delete mode 100644 docs/design/provenance_proposal.md delete mode 100644 docs/design/user_stories.md delete mode 100644 docs/pushing.md delete mode 100644 docs/pushing_charts.md delete mode 100644 docs/templates/registry.md delete mode 100644 docs/test-architecture.md delete mode 100644 docs/usage_docs/authoring_charts.md delete mode 100644 docs/usage_docs/getting-started-guide.md delete mode 100644 docs/workflow/developer-workflows.md delete mode 100644 docs/workflow/helm-dm-diagrams.src.md delete mode 100644 docs/workflow/helm-official-workflow.png delete mode 100644 docs/workflow/private-chart-no-repo.png delete mode 100644 docs/workflow/private-chart-repo.png delete mode 100644 docs/workflow/public-chart-repo.png delete mode 100644 docs/workflow/team-workflows.md delete mode 100644 examples/charts/nginx/Chart.yaml delete mode 100644 examples/charts/nginx/LICENSE delete mode 100644 examples/charts/nginx/README.md delete mode 100644 examples/charts/nginx/templates/nginx-rc.yaml delete mode 100644 examples/charts/nginx/templates/nginx-svc.yaml delete mode 100644 examples/charts/redis/Chart.yaml delete mode 100644 examples/charts/redis/templates/redis.jinja delete mode 100644 examples/charts/redis/templates/redis.jinja.schema delete mode 100644 examples/charts/replicatedservice/Chart.yaml delete mode 100644 examples/charts/replicatedservice/templates/replicatedservice.py delete mode 100644 examples/charts/replicatedservice/templates/replicatedservice.py.schema delete mode 100644 examples/guestbook/README.md delete mode 100644 examples/guestbook/guestbook.yaml delete mode 100644 examples/package/cassandra.yaml delete mode 100644 examples/wordpress/README.md delete mode 100644 examples/wordpress/architecture.png delete mode 100644 examples/wordpress/images/nginx/Dockerfile delete mode 100644 examples/wordpress/images/nginx/Makefile delete mode 100644 examples/wordpress/images/nginx/default.conf delete mode 100644 examples/wordpress/wordpress-resources.yaml delete mode 100644 examples/wordpress/wordpress.jinja delete mode 100644 examples/wordpress/wordpress.jinja.schema delete mode 100644 examples/wordpress/wordpress.yaml delete mode 100755 expansion/expansion.py delete mode 100644 expansion/expansion_test.py delete mode 100644 expansion/file_expander.py delete mode 100644 expansion/requirements.txt delete mode 100644 expansion/sandbox_loader.py delete mode 100644 expansion/schema_validation.py delete mode 100644 expansion/schema_validation_test.py delete mode 100644 expansion/schema_validation_utils.py delete mode 100755 get-install.sh delete mode 100644 glide.lock delete mode 100644 glide.yaml delete mode 100644 hack/README.md delete mode 100644 hack/Vagrantfile delete mode 100644 pkg/chart/chart.go delete mode 100644 pkg/chart/chart_test.go delete mode 100644 pkg/chart/chartfile.go delete mode 100644 pkg/chart/chartfile_test.go delete mode 100644 pkg/chart/doc.go delete mode 100644 pkg/chart/locator.go delete mode 100644 pkg/chart/locator_test.go delete mode 100644 pkg/chart/save.go delete mode 100644 pkg/chart/save_test.go delete mode 100644 pkg/chart/testdata/README.md delete mode 100644 pkg/chart/testdata/frobnitz-0.0.1.tgz delete mode 100644 pkg/chart/testdata/frobnitz/Chart.yaml delete mode 100644 pkg/chart/testdata/frobnitz/LICENSE delete mode 100644 pkg/chart/testdata/frobnitz/README.md delete mode 100644 pkg/chart/testdata/frobnitz/docs/README.md delete mode 100644 pkg/chart/testdata/frobnitz/hooks/pre-install.py delete mode 100644 pkg/chart/testdata/frobnitz/icon.svg delete mode 100644 pkg/chart/testdata/frobnitz/templates/wordpress-resources.yaml delete mode 100644 pkg/chart/testdata/frobnitz/templates/wordpress.jinja delete mode 100644 pkg/chart/testdata/frobnitz/templates/wordpress.jinja.schema delete mode 100644 pkg/chart/testdata/frobnitz/templates/wordpress.yaml delete mode 100644 pkg/chart/testdata/ill-1.2.3.tgz delete mode 100644 pkg/chart/testdata/ill/Chart.yaml delete mode 100644 pkg/chart/testdata/ill/LICENSE delete mode 100644 pkg/chart/testdata/ill/README.md delete mode 100644 pkg/chart/testdata/ill/docs/README.md delete mode 100644 pkg/chart/testdata/ill/hooks/pre-install.py delete mode 100644 pkg/chart/testdata/ill/templates/wordpress-resources.yaml delete mode 100644 pkg/chart/testdata/ill/templates/wordpress.jinja delete mode 100644 pkg/chart/testdata/ill/templates/wordpress.jinja.schema delete mode 100644 pkg/chart/testdata/ill/templates/wordpress.yaml delete mode 100644 pkg/chart/testdata/nochart.tgz delete mode 100644 pkg/chart/testdata/sprocket/Chart.yaml delete mode 100644 pkg/chart/testdata/sprocket/LICENSE delete mode 100644 pkg/chart/testdata/sprocket/README.md delete mode 100644 pkg/chart/testdata/sprocket/docs/README.md delete mode 100644 pkg/chart/testdata/sprocket/hooks/pre-install.py delete mode 100644 pkg/chart/testdata/sprocket/icon.svg delete mode 100644 pkg/chart/testdata/sprocket/templates/placeholder.txt delete mode 100644 pkg/client/client.go delete mode 100644 pkg/client/client_test.go delete mode 100644 pkg/client/deployments.go delete mode 100644 pkg/client/deployments_test.go delete mode 100644 pkg/client/install.go delete mode 100644 pkg/client/transport.go delete mode 100644 pkg/client/transport_test.go delete mode 100644 pkg/client/uninstall.go delete mode 100644 pkg/common/types.go delete mode 100644 pkg/doc.go delete mode 100644 pkg/expansion/service.go delete mode 100644 pkg/expansion/service_test.go delete mode 100644 pkg/expansion/types.go delete mode 100644 pkg/expansion/validate.go delete mode 100644 pkg/expansion/validate_test.go delete mode 100644 pkg/format/messages.go delete mode 100644 pkg/format/messages_test.go delete mode 100644 pkg/httputil/doc.go delete mode 100644 pkg/httputil/encoder.go delete mode 100644 pkg/httputil/encoder_test.go delete mode 100644 pkg/httputil/httperrors.go delete mode 100644 pkg/httputil/httperrors_test.go delete mode 100644 pkg/kubectl/cluster_info.go delete mode 100644 pkg/kubectl/command.go delete mode 100644 pkg/kubectl/create.go delete mode 100644 pkg/kubectl/create_test.go delete mode 100644 pkg/kubectl/delete.go delete mode 100644 pkg/kubectl/get.go delete mode 100644 pkg/kubectl/get_test.go delete mode 100644 pkg/kubectl/kubectl.go delete mode 100644 pkg/kubectl/kubectl_test.go delete mode 100644 pkg/log/log.go delete mode 100644 pkg/log/log_test.go delete mode 100644 pkg/pkg_test.go delete mode 100644 pkg/repo/filebased_credential_provider.go delete mode 100644 pkg/repo/filebased_credential_provider_test.go delete mode 100644 pkg/repo/gcs_repo.go delete mode 100644 pkg/repo/gcs_repo_test.go delete mode 100644 pkg/repo/inmem_credential_provider.go delete mode 100644 pkg/repo/inmem_credential_provider_test.go delete mode 100644 pkg/repo/inmem_repo_service.go delete mode 100644 pkg/repo/inmem_repo_service_test.go delete mode 100644 pkg/repo/repo.go delete mode 100644 pkg/repo/repo_test.go delete mode 100644 pkg/repo/repoprovider.go delete mode 100644 pkg/repo/repoprovider_test.go delete mode 100644 pkg/repo/secrets_credential_provider.go delete mode 100644 pkg/repo/testdata/test_credentials_file.yaml delete mode 100644 pkg/repo/types.go delete mode 100644 pkg/util/httpclient.go delete mode 100644 pkg/util/httpclient_test.go delete mode 100644 pkg/util/httputil.go delete mode 100644 pkg/util/kubernetes.go delete mode 100644 pkg/util/kubernetes_kubectl.go delete mode 100644 pkg/util/kubernetesutil.go delete mode 100644 pkg/util/kubernetesutil_test.go delete mode 100644 pkg/util/templateutil.go delete mode 100644 pkg/util/templateutil_test.go delete mode 100644 pkg/version/version.go delete mode 100644 rootfs/Makefile delete mode 100644 rootfs/README.md delete mode 100644 rootfs/expandybird/.dockerignore delete mode 100644 rootfs/expandybird/Dockerfile delete mode 100644 rootfs/expandybird/Makefile delete mode 100644 rootfs/include.mk delete mode 100644 rootfs/manager/.dockerignore delete mode 100644 rootfs/manager/Dockerfile delete mode 100644 rootfs/manager/Makefile delete mode 100644 rootfs/resourcifier/.dockerignore delete mode 100644 rootfs/resourcifier/Dockerfile delete mode 100644 rootfs/resourcifier/Makefile delete mode 100755 scripts/build-go.sh delete mode 100644 scripts/cluster/kube-system.yaml delete mode 100644 scripts/cluster/skydns.yaml delete mode 100644 scripts/common.sh delete mode 100755 scripts/coverage.sh delete mode 100644 scripts/docker.sh delete mode 100755 scripts/kube-down.sh delete mode 100755 scripts/kube-up.sh delete mode 100755 scripts/kubectl.sh delete mode 100755 scripts/start-local.sh delete mode 100755 scripts/stop-local.sh delete mode 100755 scripts/validate-go.sh diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e2227a57b..000000000 --- a/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -*~ -.*.swp -*.pyc -.project -nohup.out -/.coverage -/bin -/vendor/* -/rootfs/manager/bin/manager -/rootfs/manager/bin/kubectl -/rootfs/manager/bin/v1.* -/rootfs/resourcifier/bin/resourcifier -/rootfs/resourcifier/bin/kubectl -/rootfs/resourcifier/bin/v1.* -/rootfs/expandybird/bin/expandybird -/rootfs/expandybird/opt/expansion -.DS_Store -/log/ -/scripts/env.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d1a245afe..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,81 +0,0 @@ -# Contributing Guidelines - -The Kubernetes Helm project accepts contributions via GitHub pull requests. This document outlines the process to help get your contribution accepted. - -## Contributor License Agreements - -We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles. - -Please fill out either the individual or corporate Contributor License Agreement (CLA). - - * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). - * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). - -Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. - -***NOTE***: Only original source code from you and other people that have signed the CLA can be accepted into the main repository. - -## Development Lifecycle - -The project uses a combination of milestones and priority labels on GitHub issues to help development flow smoothly. While exceptions may be required on occasion, the team observes the following guidelines: - -* At appropriate intervals, the Helm team creates a milestone and assigns - issues to it. This represents the team's priorities and intent. -* PRs/Issues related to the current milestone are prioritized over other PRs. -* PRs/Issues that fix a broken master build (or meet other P0 criteria) are - prioritized over other PRs. - -## How to Contribute A Patch - -### Overview - -1. Submit an issue describing your proposed change to the repo in question. -1. A collaborator will respond to your issue. -1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). -1. Fork the desired repo, develop and test your code changes. -1. Submit a pull request. - -### Feature Proposals - -Before adding a feature or making a major change to the code, open an -issue marked as a _proposal_ and explain your idea. For complex changes, -you may be asked to produce a design document. - -### Single Issue - -When fixing or implementing a GitHub issue, resist the temptation to refactor nearby code or to fix that potential bug you noticed. Instead, open a new pull request just for that change. - -Keeping concerns separated allows pull requests to be tested, reviewed, and merged more quickly. - -Squash and rebase the commit or commits in your pull request into logical units of work with `git`. Include tests and documentation changes in the same commit, so that a revert would remove all traces of the feature or fix. - -If a PR completely resolves an existing issue, this should be noted. In the PR description–not in the commit itself–include a line such as "Closes #1234". The issue referenced will then be closed when your PR is merged. If it otherwise relates to an existing issue, that should be noted in the comment as well. - -### Include Tests & Documentation - -If you change or add functionality, your changes should include the necessary tests to prove that it works. While working on local code changes, always run the tests. Any change that could affect a user's experience also needs a change or addition to the relevant documentation. - -Pull requests that do not include sufficient tests or documentation will be rejected. - -***NOTE***: Please note that we are currently using Go version 1.6, and tests will fail if you run them on any other version of Go. - -### Coding Standards - -Go code should always be run through `gofmt` on the default settings. Lines of code may be up to 99 characters long. Documentation strings and tests are required for all public methods. Use of third-party go packages should be minimal, but when doing so, vendor code using [Glide](http://glide.sh/). - -Python code should conform to [PEP8](https://www.python.org/dev/peps/pep-0008/). - -### Merge Approval - -Helm collaborators may add "LGTM" (Looks Good To Me) or an equivalent comment to indicate that a PR is acceptable. Any change requires at least one LGTM. No pull requests can be merged until at least one Helm collaborator signs off with an LGTM. - -If the PR is from a Helm collaborator, then he or she should be the one to merge and close it. This keeps the commit stream clean and gives the collaborator the benefit of revisiting the PR before deciding whether or not to merge the changes. - -## Support Channels - -Whether you are a user or contributor, official support channels include: - -- GitHub issues: https://github.com/kubenetes/helm/issues/new -- Slack: #Helm room in the [Kubernetes Slack](http://slack.kubernetes.io/) - -Before opening a new issue or submitting a new pull request, it's helpful to search the project - it's likely that another user has already reported the issue you're facing, or it's a known issue that we're already aware of. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d64569567..000000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/MAINTAINERS.md b/MAINTAINERS.md deleted file mode 100644 index 235618051..000000000 --- a/MAINTAINERS.md +++ /dev/null @@ -1,61 +0,0 @@ -# Helm Maintainers - -This document explains the leadership structure of the Kubernetes Helm project, and list the current project maintainers. - -## What is a Maintainer? - -(Unabashedly stolen from the [Docker](https://github.com/docker/docker/blob/master/MAINTAINERS) project) - -There are different types of maintainers, with different responsibilities, but -all maintainers have 3 things in common: - -1. They share responsibility in the project's success. -2. They have made a long-term, recurring time investment to improve the project. -3. They spend that time doing whatever needs to be done, not necessarily what -is the most interesting or fun. - -## Types of Maintainers - -The Helm project includes two types of official maintainers: maintainers and core maintainers. - -### Helm Maintainers - -Helm maintainers are developers who have commit access to the Helm repository. -The duties of a maintainer include: - -* Classify and respond to GitHub issues and review pull requests -* Perform code reviews -* Shape the Helm roadmap and lead efforts to accomplish roadmap milestones -* Participate actively in feature development and bug fixing -* Answer questions and help users -* Participate in planning meetings - -### Helm Core Maintainers - -In addition to the duties of a Maintainer, Helm Core Maintainers also: - -* Coordinate planning meetings -* Triage GitHub issues for milestone planning -* Escalate emergency issues (broken builds, security flaws) outside of - the normal planning process - -The current core maintainers of Helm: - -* Jack Greenfield - [@jackgr](https://github.com/jackgr) -* Matt Butcher - [@technosophos](https://github.com/technosophos) - -## Project Planning - -The Helm team holds regular planning meetings to set the project direction, milestones, and relative prioritization of issues. Planning meetings are coordinated via the #Helm room in the [Kubernetes Slack](http://slack.kubernetes.io/). - -In order to solicit feedback from the community, planning meetings are run in public whenever possible. - -## Becoming a Maintainer - -Generally, potential maintainers are selected by the existing core maintainers based in part on the following criteria: - -* Sustained contributions to the project over a period of time (usually months) -* A willingness to help users on GitHub and in the [#Helm Slack room](http://slack.kubernetes.io/) -* A friendly attitude - -The Helm core maintainers must unanimously agree before inviting a community member to join as a maintainer, although in many cases the candidate has already been acting in the capacity of a maintainer for some time, and has been consulted on issues, pull requests, etc. \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index aba820c6b..000000000 --- a/Makefile +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -GO_DIRS ?= $(shell glide nv -x ) -GO_PKGS ?= $(shell glide nv) - -ROOTFS := rootfs -CLIENT := cmd/helm - -.PHONY: info -info: - $(MAKE) -C $(ROOTFS) $@ - -.PHONY: gocheck -ifndef GOPATH - $(error No GOPATH set) -endif - -.PHONY: build -build: gocheck - @scripts/build-go.sh - -.PHONY: build-static -build-static: gocheck - @BUILD_TYPE=STATIC scripts/build-go.sh - -.PHONY: build-cross -build-cross: gocheck - @BUILD_TYPE=CROSS scripts/build-go.sh - -.PHONY: all -all: build - -.PHONY: clean -clean: - $(MAKE) -C $(ROOTFS) $@ - go clean -v $(GO_PKGS) - rm -rf bin - -.PHONY: test -test: build test-style test-unit test-flake8 - -.PHONY: quicktest -quicktest: test-style - go test $(GO_PKGS) - -.PHONY: push -push: push-server push-client - -.PHONY: push-server -push-server: build-static - $(MAKE) -C $(ROOTFS) push - -.PHONY: push-client -push-client: gocheck - @BUILD_TYPE=CROSS scripts/build-go.sh $(CLIENT) - $(MAKE) -C $(CLIENT) push - -.PHONY: container -container: build-static - $(MAKE) -C $(ROOTFS) $@ - -.PHONY: test-unit -test-unit: - @echo Running tests... - go test -race -v $(GO_PKGS) - -.PHONY: test-flake8 -test-flake8: - @echo Running flake8... - flake8 expansion - @echo ---------------- - -.PHONY: test-style -test-style: - @scripts/validate-go.sh - -HAS_GLIDE := $(shell command -v glide;) -HAS_GOLINT := $(shell command -v golint;) -HAS_GOVET := $(shell command -v go tool vet;) -HAS_GOX := $(shell command -v gox;) -HAS_PIP := $(shell command -v pip;) -HAS_FLAKE8 := $(shell command -v flake8;) - -.PHONY: bootstrap -bootstrap: - @echo Installing deps -ifndef HAS_PIP - $(error Please install the latest version of Python pip) -endif -ifndef HAS_GLIDE - go get -u github.com/Masterminds/glide -endif -ifndef HAS_GOLINT - go get -u github.com/golang/lint/golint -endif -ifndef HAS_GOVET - go get -u golang.org/x/tools/cmd/vet -endif -ifndef HAS_GOX - go get -u github.com/mitchellh/gox -endif -ifndef HAS_FLAKE8 - pip install flake8 -endif - glide install - pip install --user -r expansion/requirements.txt diff --git a/README.md b/README.md deleted file mode 100644 index 31cd2761d..000000000 --- a/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# Helm - -[](https://circleci.com/gh/kubernetes/helm) [](http://goreportcard.com/report/kubernetes/helm) - -Helm makes it easy to create, describe, update and -delete Kubernetes resources using declarative configuration. A configuration is -just a `YAML` file that configures Kubernetes resources or supplies parameters -to templates. - -Helm Manager runs server side, in your Kubernetes cluster, so it can tell you what templates -you've instantiated there, what resources they created, and even how the resources -are organized. So, for example, you can ask questions like: - -* What Redis instances are running in this cluster? -* What Redis master and slave services are part of this Redis instance? -* What pods are part of this Redis slave? - -The official Helm repository of charts is available in the -[kubernetes/charts](https://github.com/kubernetes/charts) repository. - -Please hang out with us in [the Slack chat room](https://kubernetes.slack.com/messages/helm/). - -## Installing Helm - -Note: if you're exploring or using the project, you'll probably want to pull -[the latest release](https://github.com/kubernetes/helm/releases/latest), -since there may be undiscovered or unresolved issues at HEAD. - -From a Linux or Mac OS X client: - -``` -$ git clone https://github.com/kubernetes/helm.git -$ cd helm -$ make build -$ bin/helm server install -``` - -That's it. You can now use `kubectl` to see Helm running in your cluster like this: - -``` -$ kubectl get pod,rc,service --namespace=helm -NAME READY STATUS RESTARTS AGE -expandybird-rc-e0whp 1/1 Running 0 35m -expandybird-rc-zdp8w 1/1 Running 0 35m -manager-rc-bl4i4 1/1 Running 0 35m -resourcifier-rc-21clg 1/1 Running 0 35m -resourcifier-rc-i2zhi 1/1 Running 0 35m -NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE -expandybird-service 10.0.0.248 <none> 8081/TCP 35m -manager-service 10.0.0.49 <none> 8080/TCP 35m -resourcifier-service 10.0.0.184 <none> 8082/TCP 35m -NAME DESIRED CURRENT AGE -expandybird-rc 2 2 35m -manager-rc 1 1 35m -resourcifier-rc 2 2 35m -``` - -If you see expandybird, manager and resourcifier services, as well as expandybird, manager and resourcifier replication controllers with pods that are READY, then Helm is up and running! - -## Using Helm - -Run a Kubernetes proxy to allow the Helm client to connect to the remote cluster: - -``` -kubectl proxy --port=8001 & -``` - -Configure the HELM_HOST environment variable to let the local Helm client talk to the Helm manager service running in your remote Kubernetes cluster using the proxy. - -``` -export HELM_HOST=http://localhost:8001/api/v1/proxy/namespaces/helm/services/manager-service:manager -``` - -## Installing Charts - -To quickly deploy a chart, you can use the Helm command line tool. - -Currently here is the step by step guide. - -First add a respository of Charts used for testing: - -``` -$ bin/helm repo add kubernetes-charts-testing gs://kubernetes-charts-testing -``` - -Then deploy a Chart from this repository. For example to start a Redis cluster: - -``` -$ bin/helm deploy --name test --properties "workers=2" gs://kubernetes-charts-testing/redis-2.0.0.tgz -``` -The command above will create a helm "deployment" called `test` using the `redis-2.0.0.tgz` chart stored in the google storage bucket `kubernetes-charts-testing`. - -`$ bin/helm deployment describe test` will allow you to see the status of the resources you just created using the redis-2.0.0.tgz chart. You can also use kubectl to see the the same resources. It'll look like this: - -``` -$ kubectl get pods,svc,rc -NAME READY STATUS RESTARTS AGE -barfoo-barfoo 5/5 Running 0 45m -redis-master-rc-8wrqt 1/1 Running 0 41m -redis-slave-rc-6ptx6 1/1 Running 0 41m -redis-slave-rc-yc12q 1/1 Running 0 41m -NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE -kubernetes 10.0.0.1 <none> 443/TCP 45m -redis-master 10.0.0.67 <none> 6379/TCP 41m -redis-slave 10.0.0.168 <none> 6379/TCP 41m -NAME DESIRED CURRENT AGE -redis-master-rc 1 1 41m -redis-slave-rc 2 2 41m -``` - -To connect to your Redis master with a local `redis-cli` just use `kubectl port-forward` in a similar manner to: - -``` -$ kubectl port-forward redis-master-rc-8wrqt 6379:639 & -$ redis-cli -127.0.0.1:6379> info -... -role:master -connected_slaves:2 -slave0:ip=172.17.0.10,port=6379,state=online,offset=925,lag=0 -slave1:ip=172.17.0.11,port=6379,state=online,offset=925,lag=1 -``` - -Once you are done, you can delete your deployment with - -``` -$ bin/helm deployment list -test -$ bin/helm deployment rm test -```` - -## Uninstalling Helm from Kubernetes - -You can uninstall Helm entirely using the following command: - -``` -$ bin/helm server uninstall -``` - -This command will remove everything in the Helm namespace being used. - -## Design of Helm - -There is a more detailed [design document](docs/design/design.md) available. - -## Status of the Project - -This project is still under active development, so you might run into issues. If -you do, please don't be shy about letting us know, or better yet, contribute a -fix or feature. - -## Contributing -Your contributions are welcome. - -We use the same [workflow](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/development.md#git-setup), -[License](LICENSE) and [Contributor License Agreement](CONTRIBUTING.md) as the main Kubernetes repository. - -## Relationship to Google Cloud Platform's Deployment Manager and Deis's Helm -Kubernetes Helm represent a merge of Google's Deployment Manager (DM) and the original Helm from Deis. -Kubernetes Helm uses many of the same concepts and languages as -[Google Cloud Deployment Manager](https://cloud.google.com/deployment-manager/overview), -but creates resources in Kubernetes clusters, not in Google Cloud Platform projects. It also brings several concepts from the original Helm such as Charts. diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c66114bea..000000000 --- a/circle.yml +++ /dev/null @@ -1,33 +0,0 @@ -machine: - environment: - GLIDE_VERSION: "0.10.1" - GO15VENDOREXPERIMENT: 1 - GOPATH: /usr/local/go_workspace - HOME: /home/ubuntu - IMPORT_PATH: "github.com/kubernetes/helm" - PATH: $HOME/go/bin:$PATH - GOROOT: $HOME/go - -dependencies: - override: - - mkdir -p $HOME/go - - wget "https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz" - - tar -C $HOME -xzf go1.6.linux-amd64.tar.gz - - go version - - go env - - sudo chown -R $(whoami):staff /usr/local - - cd $GOPATH - - mkdir -p $GOPATH/src/$IMPORT_PATH - - cd $HOME/helm - - rsync -az --delete ./ "$GOPATH/src/$IMPORT_PATH/" - - wget "https://github.com/Masterminds/glide/releases/download/$GLIDE_VERSION/glide-$GLIDE_VERSION-linux-amd64.tar.gz" - - mkdir -p $HOME/bin - - tar -vxz -C $HOME/bin --strip=1 -f glide-$GLIDE_VERSION-linux-amd64.tar.gz - - export PATH="$HOME/bin:$PATH" GLIDE_HOME="$HOME/.glide" - - cd $GOPATH/src/$IMPORT_PATH - - sudo pip install -r expansion/requirements.txt - - sudo pip install flake8 - -test: - override: - - cd $GOPATH/src/$IMPORT_PATH && make bootstrap test diff --git a/cmd/doc.go b/cmd/doc.go deleted file mode 100644 index 8a962fc29..000000000 --- a/cmd/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -// Package cmd contains the executables for Helm. -package cmd diff --git a/cmd/expandybird/expander/expander.go b/cmd/expandybird/expander/expander.go deleted file mode 100644 index bb987163b..000000000 --- a/cmd/expandybird/expander/expander.go +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expander - -import ( - "bytes" - "encoding/json" - "fmt" - "github.com/ghodss/yaml" - "log" - "os/exec" - - "github.com/kubernetes/helm/pkg/expansion" -) - -type expander struct { - ExpansionBinary string -} - -// NewExpander returns an ExpandyBird expander. -func NewExpander(binary string) expansion.Expander { - return &expander{binary} -} - -type expandyBirdConfigOutput struct { - Resources []interface{} `yaml:"resources,omitempty"` -} - -type expandyBirdOutput struct { - Config *expandyBirdConfigOutput `yaml:"config,omitempty"` - Layout interface{} `yaml:"layout,omitempty"` -} - -// ExpandChart passes the given configuration to the expander and returns the -// expanded configuration as a string on success. -func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) { - - if err := expansion.ValidateRequest(request); err != nil { - return nil, err - } - - request, err := expansion.ValidateProperties(request) - if err != nil { - return nil, err - } - - chartInv := request.ChartInvocation - chartFile := request.Chart.Chartfile - chartMembers := request.Chart.Members - - if e.ExpansionBinary == "" { - message := fmt.Sprintf("expansion binary cannot be empty") - return nil, fmt.Errorf("%s: %s", chartInv.Name, message) - } - - entrypointIndex := -1 - for i, f := range chartMembers { - if f.Path == chartFile.Expander.Entrypoint { - entrypointIndex = i - } - } - if entrypointIndex == -1 { - message := fmt.Sprintf("The entrypoint in the chart.yaml cannot be found: %s", chartFile.Expander.Entrypoint) - return nil, fmt.Errorf("%s: %s", chartInv.Name, message) - } - - // Those are automatically increasing buffers, so writing arbitrary large - // data here won't block the child process. - var stdout bytes.Buffer - var stderr bytes.Buffer - - // Now we convert the new chart representation into the form that classic ExpandyBird takes. - - chartInvJSON, err := json.Marshal(chartInv) - if err != nil { - return nil, fmt.Errorf("error marshalling chart invocation %s: %s", chartInv.Name, err) - } - content := "{ \"resources\": [" + string(chartInvJSON) + "] }" - - cmd := &exec.Cmd{ - Path: e.ExpansionBinary, - // Note, that binary name still has to be passed argv[0]. - Args: []string{e.ExpansionBinary, content}, - Stdout: &stdout, - Stderr: &stderr, - } - - for i, f := range chartMembers { - name := f.Path - path := f.Path - if i == entrypointIndex { - // This is how expandyBird identifies the entrypoint. - name = chartInv.Type - } - cmd.Args = append(cmd.Args, name, path, string(f.Content)) - } - - if err := cmd.Start(); err != nil { - log.Printf("error starting expansion process: %s", err) - return nil, err - } - - cmd.Wait() - - log.Printf("Expansion process: pid: %d SysTime: %v UserTime: %v", cmd.ProcessState.Pid(), - cmd.ProcessState.SystemTime(), cmd.ProcessState.UserTime()) - if stderr.String() != "" { - return nil, fmt.Errorf("%s: %s", chartInv.Name, stderr.String()) - } - - output := &expandyBirdOutput{} - if err := yaml.Unmarshal(stdout.Bytes(), output); err != nil { - return nil, fmt.Errorf("cannot unmarshal expansion result (%s):\n%s", err, output) - } - - return &expansion.ServiceResponse{Resources: output.Config.Resources}, nil -} diff --git a/cmd/expandybird/expander/expander_test.go b/cmd/expandybird/expander/expander_test.go deleted file mode 100644 index df13a78c9..000000000 --- a/cmd/expandybird/expander/expander_test.go +++ /dev/null @@ -1,832 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expander - -import ( - "fmt" - "path/filepath" - "reflect" - "runtime" - "strings" - "testing" - - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/expansion" -) - -var expanderName = "../../../expansion/expansion.py" - -// content provides an easy way to provide file content verbatim in tests. -func content(lines []string) []byte { - return []byte(strings.Join(lines, "\n") + "\n") -} - -func getChartNameFromPC(pc uintptr) string { - rf := runtime.FuncForPC(pc) - fn := rf.Name() - bn := filepath.Base(fn) - split := strings.Split(bn, ".") - if len(split) > 1 { - split = split[1:] - } - - cn := fmt.Sprintf("%s-1.2.3.tgz", split[0]) - return cn -} - -func getChartURLFromPC(pc uintptr) string { - cn := getChartNameFromPC(pc) - cu := fmt.Sprintf("gs://kubernetes-charts-testing/%s", cn) - return cu -} - -func getTestChartName(t *testing.T) string { - pc, _, _, _ := runtime.Caller(1) - cu := getChartURLFromPC(pc) - cl, err := chart.Parse(cu) - if err != nil { - t.Fatalf("cannot parse chart reference %s: %s", cu, err) - } - - return cl.Name -} - -func getTestChartURL() string { - pc, _, _, _ := runtime.Caller(1) - cu := getChartURLFromPC(pc) - return cu -} - -func testExpansion(t *testing.T, req *expansion.ServiceRequest, - expResponse *expansion.ServiceResponse, expError string) { - backend := NewExpander(expanderName) - response, err := backend.ExpandChart(req) - if err != nil { - message := err.Error() - if expResponse != nil || !strings.Contains(message, expError) { - t.Fatalf("unexpected error: %v\n", err) - } - } else { - if expResponse == nil { - t.Fatalf("expected error did not occur: %s\n", expError) - } - if !reflect.DeepEqual(response, expResponse) { - message := fmt.Sprintf( - "want:\n%s\nhave:\n%s\n", expResponse, response) - t.Fatalf("output mismatch:\n%s\n", message) - } - } -} - -var pyExpander = &chart.Expander{ - Name: "ExpandyBird", - Entrypoint: "templates/main.py", -} - -var jinjaExpander = &chart.Expander{ - Name: "ExpandyBird", - Entrypoint: "templates/main.jinja", -} - -func TestEmptyJinja(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{"resources:"}), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{}, - }, - "", // Error - ) -} - -func TestEmptyPython(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: pyExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.py", - Content: content([]string{ - "def GenerateConfig(ctx):", - " return 'resources:'", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{}, - }, - "", // Error - ) -} - -func TestSimpleJinja(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "- name: foo", - " type: bar", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "type": "bar", - }, - }, - }, - "", // Error - ) -} - -func TestSimplePython(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: pyExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.py", - Content: content([]string{ - "def GenerateConfig(ctx):", - " return '''resources:", - "- name: foo", - " type: bar", - "'''", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "type": "bar", - }, - }, - }, - "", // Error - ) -} - -func TestPropertiesJinja(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "- name: foo", - " type: {{ properties.prop2 }}", - " properties:", - " something: {{ properties.prop1 }}", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "properties": map[string]interface{}{ - "something": 3.0, - }, - "type": "foo", - }, - }, - }, - "", // Error - ) -} - -func TestPropertiesPython(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: pyExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.py", - Content: content([]string{ - "def GenerateConfig(ctx):", - " return '''resources:", - "- name: foo", - " type: %(prop2)s", - " properties:", - " something: %(prop1)s", - "''' % ctx.properties", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "properties": map[string]interface{}{ - "something": 3.0, - }, - "type": "foo", - }, - }, - }, - "", // Error - ) -} - -func TestMultiFileJinja(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{"{% include 'templates/secondary.jinja' %}"}), - }, - { - Path: "templates/secondary.jinja", - Content: content([]string{ - "resources:", - "- name: foo", - " type: bar", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "type": "bar", - }, - }, - }, - "", // Error - ) -} - -var schemaContent = content([]string{ - `{`, - ` "required": ["prop1", "prop2"],`, - ` "additionalProperties": false,`, - ` "properties": {`, - ` "prop1": {`, - ` "description": "Nice description.",`, - ` "type": "integer"`, - ` },`, - ` "prop2": {`, - ` "description": "Nice description.",`, - ` "type": "string"`, - ` }`, - ` }`, - `}`, -}) - -func TestSchema(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - Schema: "Schema.yaml", - }, - Members: []*chart.Member{ - { - Path: "Schema.yaml", - Content: schemaContent, - }, - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "- name: foo", - " type: {{ properties.prop2 }}", - " properties:", - " something: {{ properties.prop1 }}", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "properties": map[string]interface{}{ - "something": 3.0, - }, - "type": "foo", - }, - }, - }, - "", // Error - ) -} - -func TestSchemaFail(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop3": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - Schema: "Schema.yaml", - }, - Members: []*chart.Member{ - { - Path: "Schema.yaml", - Content: schemaContent, - }, - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "- name: foo", - " type: {{ properties.prop2 }}", - " properties:", - " something: {{ properties.prop1 }}", - }), - }, - }, - }, - }, - nil, // Response. - `"prop2" property is missing and required`, - ) -} - -func TestMultiFileJinjaMissing(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{"{% include 'templates/secondary.jinja' %}"}), - }, - }, - }, - }, - nil, // Response - "TemplateNotFound: templates/secondary.jinja", - ) -} - -func TestMultiFilePython(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: pyExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.py", - Content: content([]string{ - "from templates import second", - "import templates.third", - "def GenerateConfig(ctx):", - " t2 = second.Gen()", - " t3 = templates.third.Gen()", - " return t2", - }), - }, - { - Path: "templates/second.py", - Content: content([]string{ - "def Gen():", - " return '''resources:", - "- name: foo", - " type: bar", - "'''", - }), - }, - { - Path: "templates/third.py", - Content: content([]string{ - "def Gen():", - " return '''resources:", - "- name: foo", - " type: bar", - "'''", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "type": "bar", - }, - }, - }, - "", // Error - ) -} - -func TestMultiFilePythonMissing(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: pyExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.py", - Content: content([]string{ - "from templates import second", - }), - }, - }, - }, - }, - nil, // Response - "cannot import name second", // Error - ) -} - -func TestWrongChartName(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: "WrongName", - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{"resources:"}), - }, - }, - }, - }, - nil, // Response - "does not match provided chart", - ) -} - -func TestEntrypointNotFound(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{}, - }, - }, - nil, // Response - "The entrypoint in the chart.yaml cannot be found", - ) -} - -func TestMalformedResource(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "fail", - }), - }, - }, - }, - }, - nil, // Response - "could not found expected ':'", // [sic] - ) -} - -func TestResourceNoName(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "- type: bar", - }), - }, - }, - }, - }, - nil, // Response. - "Resource does not have a name", - ) -} - -func TestResourceNoType(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: getTestChartURL(), - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: getTestChartName(t), - Expander: jinjaExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.jinja", - Content: content([]string{ - "resources:", - "- name: foo", - }), - }, - }, - }, - }, - nil, // Response. - "Resource does not have type defined", - ) -} - -func TestReplicatedService(t *testing.T) { - replicatedService, err := chart.LoadDir("../../../examples/charts/replicatedservice") - if err != nil { - t.Fatal(err) - } - replicatedServiceContent, err := replicatedService.LoadContent() - if err != nil { - t.Fatal(err) - } - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: "gs://kubernetes-charts-testing/replicatedservice-1.2.3.tgz", - Properties: map[string]interface{}{ - "image": "myimage", - "container_port": 1234, - "replicas": 3, - }, - }, - Chart: replicatedServiceContent, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "test_invocation-rc", - "properties": map[string]interface{}{ - "apiVersion": "v1", - "kind": "ReplicationController", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "name": "test_invocation-rc", - }, - "name": "test_invocation-rc", - "namespace": "default", - }, - "spec": map[string]interface{}{ - "replicas": 3.0, - "selector": map[string]interface{}{ - "name": "test_invocation", - }, - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "name": "test_invocation", - }, - }, - "spec": map[string]interface{}{ - "containers": []interface{}{ - map[string]interface{}{ - "env": []interface{}{}, - "image": "myimage", - "name": "test_invocation", - "ports": []interface{}{ - map[string]interface{}{ - "containerPort": 1234.0, - "name": "test_invocation", - }, - }, - }, - }, - }, - }, - }, - }, - "type": "ReplicationController", - }, - map[string]interface{}{ - "name": "test_invocation-service", - "properties": map[string]interface{}{ - "apiVersion": "v1", - "kind": "Service", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "name": "test_invocation-service", - }, - "name": "test_invocation-service", - "namespace": "default", - }, - "spec": map[string]interface{}{ - "ports": []interface{}{ - map[string]interface{}{ - "name": "test_invocation", - "port": 1234.0, - "targetPort": 1234.0, - }, - }, - "selector": map[string]interface{}{ - "name": "test_invocation", - }, - }, - }, - "type": "Service", - }, - }, - }, - "", // Error. - ) -} diff --git a/cmd/expandybird/main.go b/cmd/expandybird/main.go deleted file mode 100644 index 7160eb459..000000000 --- a/cmd/expandybird/main.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "github.com/kubernetes/helm/cmd/expandybird/expander" - "github.com/kubernetes/helm/pkg/expansion" - "github.com/kubernetes/helm/pkg/version" - - "flag" - "log" -) - -// interface that we are going to listen on -var address = flag.String("address", "", "Interface to listen on") - -// port that we are going to listen on -var port = flag.Int("port", 8080, "Port to listen on") - -// path to expansion binary -var expansionBinary = flag.String("expansion_binary", "../../../expansion/expansion.py", - "The path to the expansion binary that will be used to expand the template.") - -func main() { - flag.Parse() - backend := expander.NewExpander(*expansionBinary) - service := expansion.NewService(*address, *port, backend) - log.Printf("Version: %s", version.Version) - log.Printf("Listening on http://%s:%d/expand", *address, *port) - log.Fatal(service.ListenAndServe()) -} diff --git a/cmd/expandybird/test/ExpectedOutput.yaml b/cmd/expandybird/test/ExpectedOutput.yaml deleted file mode 100644 index 02bf868b8..000000000 --- a/cmd/expandybird/test/ExpectedOutput.yaml +++ /dev/null @@ -1,77 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -config: - resources: - - name: expandybird-service - properties: - apiVersion: v1 - kind: Service - metadata: - labels: - app: expandybird - name: expandybird-service - namespace: default - spec: - ports: - - name: expandybird - port: 8080 - targetPort: 8080 - selector: - app: expandybird - type: LoadBalancer - type: Service - - name: expandybird-rc - properties: - apiVersion: v1 - kind: ReplicationController - metadata: - labels: - app: expandybird - name: expandybird-rc - namespace: default - spec: - replicas: 3 - selector: - app: expandybird - template: - metadata: - labels: - app: expandybird - spec: - containers: - - env: [] - image: gcr.io/kubernetes-helm/expandybird - name: expandybird - ports: - - containerPort: 8080 - name: expandybird - type: ReplicationController -layout: - resources: - - name: expandybird - properties: - container_port: 8080 - external_service: true - image: gcr.io/kubernetes-helm/expandybird - labels: - app: expandybird - replicas: 3 - service_port: 8080 - target_port: 8080 - resources: - - name: expandybird-service - type: Service - - name: expandybird-rc - type: ReplicationController - type: replicatedservice.py \ No newline at end of file diff --git a/cmd/expandybird/test/InvalidFileName.yaml b/cmd/expandybird/test/InvalidFileName.yaml deleted file mode 100644 index fa29b84d2..000000000 --- a/cmd/expandybird/test/InvalidFileName.yaml +++ /dev/null @@ -1,22 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: invalidfilename.py -resources: -- name: expandybird - type: replicatedservice.py - properties: - service_port: 8080 - target_port: 8080 - image: gcr.io/kubernetes-helm/expandybird diff --git a/cmd/expandybird/test/InvalidProperty.yaml b/cmd/expandybird/test/InvalidProperty.yaml deleted file mode 100644 index aab9b6341..000000000 --- a/cmd/expandybird/test/InvalidProperty.yaml +++ /dev/null @@ -1,22 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: replicatedservice.py -resources: -- name: expandybird - type: replicatedservice.py - properties: - service_port: 8080 - target_port: 8080 - invalidproperty: gcr.io/kubernetes-helm/expandybird diff --git a/cmd/expandybird/test/InvalidTypeName.yaml b/cmd/expandybird/test/InvalidTypeName.yaml deleted file mode 100644 index 67b7050f0..000000000 --- a/cmd/expandybird/test/InvalidTypeName.yaml +++ /dev/null @@ -1,22 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: replicatedservice.py -resources: -- name: expandybird - type: invalidtypename.py - properties: - service_port: 8080 - target_port: 8080 - image: gcr.io/kubernetes-helm/expandybird diff --git a/cmd/expandybird/test/MalformedContent.yaml b/cmd/expandybird/test/MalformedContent.yaml deleted file mode 100644 index c96ae41d2..000000000 --- a/cmd/expandybird/test/MalformedContent.yaml +++ /dev/null @@ -1,20 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: replicatedservice.py -resources: -- name: expandybird - type: replicatedservice.py - thisisnotalist: somevalue - shouldnotbehere: anothervalue diff --git a/cmd/expandybird/test/MissingImports.yaml b/cmd/expandybird/test/MissingImports.yaml deleted file mode 100644 index 91c11fe33..000000000 --- a/cmd/expandybird/test/MissingImports.yaml +++ /dev/null @@ -1,21 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -resources: -- name: expandybird - type: replicatedservice.py - properties: - service_port: 8080 - target_port: 8080 - image: gcr.io/kubernetes-helm/expandybird diff --git a/cmd/expandybird/test/MissingResourceName.yaml b/cmd/expandybird/test/MissingResourceName.yaml deleted file mode 100644 index 00f7e7d61..000000000 --- a/cmd/expandybird/test/MissingResourceName.yaml +++ /dev/null @@ -1,21 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: replicatedservice.py -resources: -- type: replicatedservice.py - properties: - service_port: 8080 - target_port: 8080 - image: gcr.io/kubernetes-helm/expandybird diff --git a/cmd/expandybird/test/MissingTypeName.yaml b/cmd/expandybird/test/MissingTypeName.yaml deleted file mode 100644 index 4a867f86c..000000000 --- a/cmd/expandybird/test/MissingTypeName.yaml +++ /dev/null @@ -1,21 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: replicatedservice.py -resources: -- name: expandybird - properties: - service_port: 8080 - target_port: 8080 - image: gcr.io/kubernetes-helm/expandybird diff --git a/cmd/expandybird/test/TestArchive.tar b/cmd/expandybird/test/TestArchive.tar deleted file mode 100644 index 9cb986b978e50715ef04142faa80f1bf381f1f49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9732 zcmeHMZBH9X63%D;iV~4-Kxd30Kvq8G153C&Ygt$Un<zpEwa0DK$9Trq(_><x{P%mR zUuN3IK$PQ7`G6&uneM9Udh42}dOnFv4G)Xl$lP@1Vv@ai`Cqr&-QV4%H}7|M-tX?w zo9_1Z{&p8v_uJ|2?9ukl{{G(n`*-_$+tl6O#qSMuUtX1m606b(4ORXn(%Z3qI3Gr2 zJ`T4lug;cW6s@HB2?yW4$glO54vXnrtMQGYo$mG?UEE0etr|$3OCw7<sLV~Fam_Na zR3&LyN<EXq4w#QsD)Ukfsmg~^lL7O=RHV50nKpfrx>Q9@JDo1AGr^jlTHE*=B+QG7 zCSp!`VW=u4M5vNRDw8C?rgCaX<&+kaX{JP;O1e|##xCQ9uo7Rqh+<#_s0HY!b1FtL z9SH+U{@oZe?QLz{-Q9JBRkc&-@mA&xDYuUMhes#pN1IR=41eaCEZO9LS4zXo!JNbt zx};(N4Kr~^g(fl95_6_tUGKCqDj&D0EJo%|XbCApRT`}Zm056<w}4=8OaNDjoYoG` zseiskzaE_T&)bmkxqtD`(?2ih^TFBK!O2Dc=$uZ^=<xL9WB;OmdV;6F)4|CX`mKNR zu}u<gLOkTxDWX-fl8RmEf_5(1&2@-qglT>*r!rL|mBOxkT#2!yaWO+8z#N)NJy9hG zvV?j=D3qy*GQ!xACi+cSUM4?V(7Vv4?6o#&D$Gp}v9SJ!FF6enw$60kf|W(3Q)%gP zF_AsC9g5BeN)KB^W<JH(N?xL=E?}Qg%+4v|1z+>ZJ^H2lOV<vg-i)PL9!*h$k#<rq zPspzZ&J)?S7uO@BD`_QpJ<1-vv(IV*Z}(`N>W(V5ZtLy0c_XvQ)}keZ@C;<;RVB{H z1!I50TJ@^Z|HcEW()p!VdKv?4zuVjCdJM3?zYB1HcgW7WomUL-(s5WNuk|7|_AQF} zD+qW60Y5PaSX*2BC`Wk70({U}4FJv^0I)B2hGwaj03<*wg90c~P?!e}U`%y(*lD#c z7++3`VFmnV>8(t_17Nz0!~jT5q>O4TEYekO;g1NIfFYh}MF2U?(1=LgCPp0@;9(6i zSO$XwU1S-sm{p<{1LS#&cmA+t3SbJxYAOeS#5&L)s#Uo%ofhnFc?3k<<F#7nM`xda zP_Hk({Bd-BczS$%bja|JK2YMVO<HG%tEPk7pcc1{POGHVY7OOx{viQigprsTsqs3i z4fwLba0oht+1UGRT_eX^HxVYi;Q)AD;BTS>J@#P5i|x^c1RBM1)<0ON0|wwv^bHeX z{q89vWM)O3NmJ?EOFOXZTj)i<i(HLlX-Ke+Uk-jhCWhSkxZ^CqfAk4F49Tz%cvl#@ zlZj@K%NoP^E*<RpHi6C#=`X<&2=JUA=pI}iZYDjty!ysga`+JiuXso1v&)3Xk}JpR zG2#+ggFHJ(gY|^j9hkJdE~_@Y_!3DIud&R`ES4r|Q-Y|9Dl^FjEBBV%_JP^3wRF}q z<PSR}MWX`G4T{Quymb5O`Wa<oGAwDS|4PZGAIH*-D8}D#&m-ykq>fSI#u>M+(+yhG zTc<(gS{I!r&;k=)frop8*Y+Ai!FxRM&&946W~|(Hm}6Ope6XJ%>$xT<pa{~eL@`yq z7sgC7+fD+HoxN50kjKxR?-wU0(ukojVoBt4it;@FtT8S#dNxe<K3?8ZxxNBxg#CVO z8;wv6(9%YEu@SR>iysp1BUfo^8#zo|2KnCBKe)kbR$1Hw8&|Pxt7j=QnVLdBp()3o z(=YtK`Fp<dUx_}hnqJo}S~iIGZWBVcDQhZkKXUz~$gO-eS|MtoiM&i2amm%Ca}KUH zhpw<{a$JbvuL3=HDm5pot238Tk-5T2#Z>0Qb+3Tk(>h&|u6_pum$_BY_qzPAha;7K zC`qF&dDy&5f_NLG??~AtWkal9DqFqyA4pW~5Jrgv<7DI)RO2xYj6H!UlOIpb*j3hD z#=j9)?3?G@S^MqT{cCr{^3u${uzzcLkL=@mzTT+ps7t@Y!fbqv%V0bn?i0K%qs@Gj z&q6g=2wotMuc1!Yx-3-cm@>Xk?19T8;dOHlVjJ`GeNvaRCoB(4&v_uw{g^p5ckKl( zsgIaf)7bN>hdCUC#n(_i<FWYu>IoM-#=DAVnKph7_k6X83x)v6t*n!P2B6XwEn48* z8CH`iClxqQJr^+V@>|aCk_X7wZLV;zI?~0&o|&>Ym+OPyhgW>;>Krrn@S4G?2WIXR zj}lb&q{mA#4*4u`7Je%&m7JpZH0Rh)E>z-u$bg(Xh*_cdz%x@hAjO#b6$X0TJ<za7 zs|mgrwdw9gr8m?k93(0^lW^6r!s#ilz!?LTDH9IRA<CUU@(H&uQXT4#aA5BbA_>@$ z#%d<>HU&c?I~xnXw;KrRrRdq%Y(CUy8%AiPiaR!{smAVITj!#W(k8i}vO|JO?BAvS zVW?6QR$#H7n_D~!lX^3eRlKf7V*_?>$8eN9nO=L!2Jbf-In3Y)BBX$1Y`E^7Q|uy~ z&wvpMjUT>f0R*f!3$*cNAa!jR*}mjf&fDa)VyD!abQF3}{7uSr+qAJmniVg>#!$mD zVmZGt8iJ)Au^N}H-WP&w9Rv(&y5_oD{y#+ALn2$uXp^|`an$)!QvF@S!B!X^{i4Hs zuwB@reQV0oY>2Zd(jw!f!mV3wRfD>mV9r@w0u=gt>{#gJl4`+>$I~h%i;^og=5gkT zD&nmzdYmzkxMtGD8?BPhT+R!(4ed;5dy#Lptl8|2^<JQ5mV|RNrf@qVQg?yHjdP$a zV?^fog0>4xVbHRK2QCSIFId$V-wQDeVOa|YAwVoj7dcl`92?DqR{TW}UoQE3Ao2)b zN5S$PM<4%1v4ajsL#P=viS$5DqQ_a4lNmln#zl{%H2R~e*ratekM3=7eK+hY4>zw| z7ZZ-Z%U+i80Vg3C;F{zfAHK1CI|Z=U1~I>)*<Em<p4<17V|8&`EQyw8p&H_~l+Qw6 ybRTe&rE|-$=E(+5LY%VrM#k&7IXATNEkX&9)>?JO{yO&RfmaW_df;dDz<&S<=6w4A diff --git a/cmd/expandybird/test/ValidContent.yaml b/cmd/expandybird/test/ValidContent.yaml deleted file mode 100644 index 5798efec3..000000000 --- a/cmd/expandybird/test/ValidContent.yaml +++ /dev/null @@ -1,27 +0,0 @@ -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -imports: -- path: replicatedservice.py -resources: -- name: expandybird - type: replicatedservice.py - properties: - service_port: 8080 - target_port: 8080 - container_port: 8080 - external_service: true - replicas: 3 - image: gcr.io/kubernetes-helm/expandybird - labels: - app: expandybird \ No newline at end of file diff --git a/cmd/expandybird/test/replicatedservice.py b/cmd/expandybird/test/replicatedservice.py deleted file mode 100644 index 231fb2640..000000000 --- a/cmd/expandybird/test/replicatedservice.py +++ /dev/null @@ -1,182 +0,0 @@ - -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - -"""Defines a ReplicatedService type by creating both a Service and an RC. - -This module creates a typical abstraction for running a service in a -Kubernetes cluster, namely a replication controller and a service packaged -together into a single unit. -""" - -import yaml - -SERVICE_TYPE_COLLECTION = 'Service' -RC_TYPE_COLLECTION = 'ReplicationController' - - -def GenerateConfig(context): - """Generates a Replication Controller and a matching Service. - - Args: - context: Template context. See schema for context properties. - - Returns: - A Container Manifest as a YAML string. - """ - # YAML config that we're going to create for both RC & Service - config = {'resources': []} - - name = context.env['name'] - container_name = context.properties.get('container_name', name) - namespace = context.properties.get('namespace', 'default') - - # Define things that the Service cares about - service_name = context.properties.get('service_name', name + '-service') - service_type = SERVICE_TYPE_COLLECTION - - # Define things that the Replication Controller (rc) cares about - rc_name = name + '-rc' - rc_type = RC_TYPE_COLLECTION - - service = { - 'name': service_name, - 'type': service_type, - 'properties': { - 'apiVersion': 'v1', - 'kind': 'Service', - 'metadata': { - 'name': service_name, - 'namespace': namespace, - 'labels': GenerateLabels(context, service_name), - }, - 'spec': { - 'ports': [GenerateServicePorts(context, container_name)], - 'selector': GenerateLabels(context, name) - } - } - } - set_up_external_lb = context.properties.get('external_service', None) - if set_up_external_lb: - service['properties']['spec']['type'] = 'LoadBalancer' - config['resources'].append(service) - - rc = { - 'name': rc_name, - 'type': rc_type, - 'properties': { - 'apiVersion': 'v1', - 'kind': 'ReplicationController', - 'metadata': { - 'name': rc_name, - 'namespace': namespace, - 'labels': GenerateLabels(context, rc_name), - }, - 'spec': { - 'replicas': context.properties['replicas'], - 'selector': GenerateLabels(context, name), - 'template': { - 'metadata': { - 'labels': GenerateLabels(context, name), - }, - 'spec': { - 'containers': [ - { - 'env': GenerateEnv(context), - 'name': container_name, - 'image': context.properties['image'], - 'ports': [ - { - 'name': container_name, - 'containerPort': context.properties['container_port'], - } - ] - } - ] - } - } - } - } - } - - config['resources'].append(rc) - return yaml.dump(config) - - -def GenerateLabels(context, name): - """Generates labels either from the context.properties['labels'] or - generates a default label 'app':name - - We make a deep copy of the context.properties['labels'] section to avoid - linking in the yaml document, which I believe reduces readability of the - expanded template. If no labels are given, generate a default 'app':name. - - Args: - context: Template context, which can contain the following properties: - labels - Labels to generate - - Returns: - A dict containing labels in a name:value format - """ - tmp_labels = context.properties.get('labels', None) - ret_labels = {'app': name} - if isinstance(tmp_labels, dict): - for key, value in tmp_labels.iteritems(): - ret_labels[key] = value - return ret_labels - - -def GenerateServicePorts(context, name): - """Generates a ports section for a service. - - Args: - context: Template context, which can contain the following properties: - service_port - Port to use for the service - target_port - Target port for the service - protocol - Protocol to use. - - Returns: - A dict containing a port definition - """ - service_port = context.properties.get('service_port', None) - target_port = context.properties.get('target_port', None) - protocol = context.properties.get('protocol') - - ports = {} - if name: - ports['name'] = name - if service_port: - ports['port'] = service_port - if target_port: - ports['targetPort'] = target_port - if protocol: - ports['protocol'] = protocol - - return ports - -def GenerateEnv(context): - """Generates environmental variables for a pod. - - Args: - context: Template context, which can contain the following properties: - env - Environment variables to set. - - Returns: - A list containing env variables in dict format {name: 'name', value: 'value'} - """ - env = [] - tmp_env = context.properties.get('env', []) - for entry in tmp_env: - if isinstance(entry, dict): - env.append({'name': entry.get('name'), 'value': entry.get('value')}) - return env diff --git a/cmd/expandybird/test/schemas/bad.jinja.schema b/cmd/expandybird/test/schemas/bad.jinja.schema deleted file mode 100644 index 825f8dcf7..000000000 --- a/cmd/expandybird/test/schemas/bad.jinja.schema +++ /dev/null @@ -1,9 +0,0 @@ -info: - title: Schema with a lots of errors in it - -imports: - -properties: - exclusiveMin: - type: integer - exclusiveMinimum: 0 diff --git a/cmd/expandybird/test/schemas/default_ref.jinja.schema b/cmd/expandybird/test/schemas/default_ref.jinja.schema deleted file mode 100644 index 51f83d2c8..000000000 --- a/cmd/expandybird/test/schemas/default_ref.jinja.schema +++ /dev/null @@ -1,14 +0,0 @@ -info: - title: Schema with a property that has a referenced default value - -imports: - -properties: - number: - $ref: '#/level/mult' - -level: - mult: - type: integer - multipleOf: 1 - default: 1 diff --git a/cmd/expandybird/test/schemas/defaults.jinja.schema b/cmd/expandybird/test/schemas/defaults.jinja.schema deleted file mode 100644 index bcb7ee34e..000000000 --- a/cmd/expandybird/test/schemas/defaults.jinja.schema +++ /dev/null @@ -1,12 +0,0 @@ -info: - title: Schema with properties that have default values - -imports: - -properties: - one: - type: integer - default: 1 - alpha: - type: string - default: alpha diff --git a/cmd/expandybird/test/schemas/defaults.py.schema b/cmd/expandybird/test/schemas/defaults.py.schema deleted file mode 100644 index bcb7ee34e..000000000 --- a/cmd/expandybird/test/schemas/defaults.py.schema +++ /dev/null @@ -1,12 +0,0 @@ -info: - title: Schema with properties that have default values - -imports: - -properties: - one: - type: integer - default: 1 - alpha: - type: string - default: alpha diff --git a/cmd/expandybird/test/schemas/invalid_default.jinja.schema b/cmd/expandybird/test/schemas/invalid_default.jinja.schema deleted file mode 100644 index e60d11148..000000000 --- a/cmd/expandybird/test/schemas/invalid_default.jinja.schema +++ /dev/null @@ -1,11 +0,0 @@ -info: - title: Schema with a required integer property that has a default string value - -imports: - -required: - - number -properties: - number: - type: integer - default: string diff --git a/cmd/expandybird/test/schemas/invalid_reference.py.schema b/cmd/expandybird/test/schemas/invalid_reference.py.schema deleted file mode 100644 index 7c3fa3e10..000000000 --- a/cmd/expandybird/test/schemas/invalid_reference.py.schema +++ /dev/null @@ -1,10 +0,0 @@ -info: - title: Schema with references to something that doesnt exist - -imports: - -properties: - odd: - type: integer - not: - $ref: '#/wheeeeeee' diff --git a/cmd/expandybird/test/schemas/invalid_reference_schema.py.schema b/cmd/expandybird/test/schemas/invalid_reference_schema.py.schema deleted file mode 100644 index 6c824568c..000000000 --- a/cmd/expandybird/test/schemas/invalid_reference_schema.py.schema +++ /dev/null @@ -1,8 +0,0 @@ -info: - title: Schema with references to something that doesnt exist - -imports: - -properties: - odd: - $ref: '#/wheeeeeee' diff --git a/cmd/expandybird/test/schemas/metadata.py.schema b/cmd/expandybird/test/schemas/metadata.py.schema deleted file mode 100644 index 3d6e1e346..000000000 --- a/cmd/expandybird/test/schemas/metadata.py.schema +++ /dev/null @@ -1,20 +0,0 @@ -info: - title: Schema with properties that have extra metadata - -imports: - -properties: - one: - type: integer - default: 1 - metadata: - gcloud: is great! - compute: is awesome - alpha: - type: string - default: alpha - metadata: - - you - - can - - do - - anything diff --git a/cmd/expandybird/test/schemas/missing_quote.py.schema b/cmd/expandybird/test/schemas/missing_quote.py.schema deleted file mode 100644 index ddd4b5bfd..000000000 --- a/cmd/expandybird/test/schemas/missing_quote.py.schema +++ /dev/null @@ -1,11 +0,0 @@ -info: - title: Schema with references - -imports: - -properties: - number: - $ref: #/number - -number: - type: integer diff --git a/cmd/expandybird/test/schemas/nested_defaults.py.schema b/cmd/expandybird/test/schemas/nested_defaults.py.schema deleted file mode 100644 index b5288c91b..000000000 --- a/cmd/expandybird/test/schemas/nested_defaults.py.schema +++ /dev/null @@ -1,33 +0,0 @@ -info: - title: VM with Disks - author: Kubernetes - description: Creates a single vm, then attaches disks to it. - -required: -- zone - -properties: - zone: - type: string - description: GCP zone - default: us-central1-a - disks: - type: array - items: - type: object - required: - - name - properties: - name: - type: string - description: Suffix for this disk - sizeGb: - type: integer - default: 100 - diskType: - type: string - enum: - - pd-standard - - pd-ssd - default: pd-standard - additionalProperties: false diff --git a/cmd/expandybird/test/schemas/numbers.py.schema b/cmd/expandybird/test/schemas/numbers.py.schema deleted file mode 100644 index eff245182..000000000 --- a/cmd/expandybird/test/schemas/numbers.py.schema +++ /dev/null @@ -1,27 +0,0 @@ -info: - title: Schema with a lots of number properties and restrictions - -imports: - -properties: - minimum0: - type: integer - minimum: 0 - exclusiveMin0: - type: integer - minimum: 0 - exclusiveMinimum: true - maximum10: - type: integer - maximum: 10 - exclusiveMax10: - type: integer - maximum: 10 - exclusiveMaximum: true - even: - type: integer - multipleOf: 2 - odd: - type: integer - not: - multipleOf: 2 diff --git a/cmd/expandybird/test/schemas/ref_nested_defaults.py.schema b/cmd/expandybird/test/schemas/ref_nested_defaults.py.schema deleted file mode 100644 index 80813b73d..000000000 --- a/cmd/expandybird/test/schemas/ref_nested_defaults.py.schema +++ /dev/null @@ -1,36 +0,0 @@ -info: - title: VM with Disks - author: Kubernetes - description: Creates a single vm, then attaches disks to it. - -required: -- zone - -properties: - zone: - type: string - description: GCP zone - default: us-central1-a - disks: - type: array - items: - $ref: '#/disk' - -disk: - type: object - required: - - name - properties: - name: - type: string - description: Suffix for this disk - sizeGb: - type: integer - default: 100 - diskType: - type: string - enum: - - pd-standard - - pd-ssd - default: pd-standard - additionalProperties: false diff --git a/cmd/expandybird/test/schemas/reference.jinja.schema b/cmd/expandybird/test/schemas/reference.jinja.schema deleted file mode 100644 index e90251c39..000000000 --- a/cmd/expandybird/test/schemas/reference.jinja.schema +++ /dev/null @@ -1,14 +0,0 @@ -info: - title: Schema with references - -imports: - -properties: - odd: - type: integer - not: - $ref: '#/even' - - -even: - multipleOf: 2 diff --git a/cmd/expandybird/test/schemas/req_default_ref.py.schema b/cmd/expandybird/test/schemas/req_default_ref.py.schema deleted file mode 100644 index 08b1da3e9..000000000 --- a/cmd/expandybird/test/schemas/req_default_ref.py.schema +++ /dev/null @@ -1,14 +0,0 @@ -info: - title: Schema with a required property that has a referenced default value - -imports: - -required: - - number -properties: - number: - $ref: '#/default_val' - -default_val: - type: integer - default: my_name diff --git a/cmd/expandybird/test/schemas/required.jinja.schema b/cmd/expandybird/test/schemas/required.jinja.schema deleted file mode 100644 index 94c8e39f8..000000000 --- a/cmd/expandybird/test/schemas/required.jinja.schema +++ /dev/null @@ -1,10 +0,0 @@ -info: - title: Schema with a required property - -imports: - -required: - - name -properties: - name: - type: string diff --git a/cmd/expandybird/test/schemas/required_default.jinja.schema b/cmd/expandybird/test/schemas/required_default.jinja.schema deleted file mode 100644 index d739e2c20..000000000 --- a/cmd/expandybird/test/schemas/required_default.jinja.schema +++ /dev/null @@ -1,11 +0,0 @@ -info: - title: Schema with a required property that has a default value - -imports: - -required: - - name -properties: - name: - type: string - default: my_name diff --git a/cmd/expandybird/test/templates/description_text.txt b/cmd/expandybird/test/templates/description_text.txt deleted file mode 100644 index 33e5dea2e..000000000 --- a/cmd/expandybird/test/templates/description_text.txt +++ /dev/null @@ -1 +0,0 @@ -"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." diff --git a/cmd/expandybird/test/templates/duplicate_names.yaml b/cmd/expandybird/test/templates/duplicate_names.yaml deleted file mode 100644 index f7386f186..000000000 --- a/cmd/expandybird/test/templates/duplicate_names.yaml +++ /dev/null @@ -1,9 +0,0 @@ -resources: -- type: compute.v1.instance - name: my_instance - properties: - zone: test-zone-a -- type: compute.v1.instance - name: my_instance - properties: - zone: test-zone-b diff --git a/cmd/expandybird/test/templates/duplicate_names_B.jinja b/cmd/expandybird/test/templates/duplicate_names_B.jinja deleted file mode 100644 index 8ae8a7923..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_B.jinja +++ /dev/null @@ -1,5 +0,0 @@ -resources: -- type: compute.v1.instance - name: B - properties: - zone: test-zone-b diff --git a/cmd/expandybird/test/templates/duplicate_names_C.jinja b/cmd/expandybird/test/templates/duplicate_names_C.jinja deleted file mode 100644 index 3aff269fe..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_C.jinja +++ /dev/null @@ -1,5 +0,0 @@ -resources: -- type: compute.v1.instance - name: C - properties: - zone: test-zone-c diff --git a/cmd/expandybird/test/templates/duplicate_names_in_subtemplates.jinja b/cmd/expandybird/test/templates/duplicate_names_in_subtemplates.jinja deleted file mode 100644 index f7386f186..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_in_subtemplates.jinja +++ /dev/null @@ -1,9 +0,0 @@ -resources: -- type: compute.v1.instance - name: my_instance - properties: - zone: test-zone-a -- type: compute.v1.instance - name: my_instance - properties: - zone: test-zone-b diff --git a/cmd/expandybird/test/templates/duplicate_names_in_subtemplates.yaml b/cmd/expandybird/test/templates/duplicate_names_in_subtemplates.yaml deleted file mode 100644 index 06e8a263c..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_in_subtemplates.yaml +++ /dev/null @@ -1,5 +0,0 @@ -imports: ["duplicate_names_in_subtemplates.jinja"] - -resources: -- name: subtemplate - type: duplicate_names_in_subtemplates.jinja diff --git a/cmd/expandybird/test/templates/duplicate_names_mixed_level.yaml b/cmd/expandybird/test/templates/duplicate_names_mixed_level.yaml deleted file mode 100644 index 7cb82e5e7..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_mixed_level.yaml +++ /dev/null @@ -1,7 +0,0 @@ -imports: ["duplicate_names_B.jinja", "duplicate_names_C.jinja"] - -resources: -- name: A - type: duplicate_names_B.jinja -- name: B - type: duplicate_names_C.jinja diff --git a/cmd/expandybird/test/templates/duplicate_names_mixed_level_result.yaml b/cmd/expandybird/test/templates/duplicate_names_mixed_level_result.yaml deleted file mode 100644 index de34fa47a..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_mixed_level_result.yaml +++ /dev/null @@ -1,22 +0,0 @@ -config: - resources: - - name: B - properties: - zone: test-zone-b - type: compute.v1.instance - - name: C - properties: - zone: test-zone-c - type: compute.v1.instance -layout: - resources: - - name: A - resources: - - name: B - type: compute.v1.instance - type: duplicate_names_B.jinja - - name: B - resources: - - name: C - type: compute.v1.instance - type: duplicate_names_C.jinja diff --git a/cmd/expandybird/test/templates/duplicate_names_parent_child.yaml b/cmd/expandybird/test/templates/duplicate_names_parent_child.yaml deleted file mode 100644 index 4c3ae6bf1..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_parent_child.yaml +++ /dev/null @@ -1,7 +0,0 @@ -imports: ["duplicate_names_B.jinja"] - -resources: -- name: A - type: duplicate_names_B.jinja -- name: B - type: compute.v1.instance diff --git a/cmd/expandybird/test/templates/duplicate_names_parent_child_result.yaml b/cmd/expandybird/test/templates/duplicate_names_parent_child_result.yaml deleted file mode 100644 index 8384a72ee..000000000 --- a/cmd/expandybird/test/templates/duplicate_names_parent_child_result.yaml +++ /dev/null @@ -1,17 +0,0 @@ -config: - resources: - - name: B - properties: - zone: test-zone-b - type: compute.v1.instance - - name: B - type: compute.v1.instance -layout: - resources: - - name: A - resources: - - name: B - type: compute.v1.instance - type: duplicate_names_B.jinja - - name: B - type: compute.v1.instance diff --git a/cmd/expandybird/test/templates/helper.jinja b/cmd/expandybird/test/templates/helper.jinja deleted file mode 100644 index d174617f4..000000000 --- a/cmd/expandybird/test/templates/helper.jinja +++ /dev/null @@ -1,5 +0,0 @@ -resources: -- name: helper - type: bar - properties: - test: {{ properties["foobar"] }} diff --git a/cmd/expandybird/test/templates/helper.jinja.schema b/cmd/expandybird/test/templates/helper.jinja.schema deleted file mode 100644 index 7bbdf5b5e..000000000 --- a/cmd/expandybird/test/templates/helper.jinja.schema +++ /dev/null @@ -1,4 +0,0 @@ -properties: - foobar: - type: string - default: Use this schema diff --git a/cmd/expandybird/test/templates/helpers/common.jinja b/cmd/expandybird/test/templates/helpers/common.jinja deleted file mode 100644 index 056435742..000000000 --- a/cmd/expandybird/test/templates/helpers/common.jinja +++ /dev/null @@ -1,3 +0,0 @@ -{%- macro GenerateMachineName(prefix='', suffix='') -%} - {{ prefix + "-" + suffix }} -{%- endmacro %} diff --git a/cmd/expandybird/test/templates/helpers/common.py b/cmd/expandybird/test/templates/helpers/common.py deleted file mode 100644 index a553e65dd..000000000 --- a/cmd/expandybird/test/templates/helpers/common.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Dummy helper methods invoked in other constructors.""" - - -def GenerateMachineName(prefix, suffix): - """Generates name of a VM.""" - return prefix + "-" + suffix diff --git a/cmd/expandybird/test/templates/helpers/extra/__init__.py b/cmd/expandybird/test/templates/helpers/extra/__init__.py deleted file mode 100644 index 27e204d9a..000000000 --- a/cmd/expandybird/test/templates/helpers/extra/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package marker file.""" diff --git a/cmd/expandybird/test/templates/helpers/extra/common2.py b/cmd/expandybird/test/templates/helpers/extra/common2.py deleted file mode 100644 index 469354b81..000000000 --- a/cmd/expandybird/test/templates/helpers/extra/common2.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Dummy helper methods invoked in other constructors.""" - - -def GenerateMachineSize(): - """Generates size of a VM.""" - return "big" diff --git a/cmd/expandybird/test/templates/invalid_config.yaml b/cmd/expandybird/test/templates/invalid_config.yaml deleted file mode 100644 index d2e04dfb4..000000000 --- a/cmd/expandybird/test/templates/invalid_config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -resources: -- name: foo properties: bar: baz diff --git a/cmd/expandybird/test/templates/jinja_defaults.jinja b/cmd/expandybird/test/templates/jinja_defaults.jinja deleted file mode 100644 index 74837f2a2..000000000 --- a/cmd/expandybird/test/templates/jinja_defaults.jinja +++ /dev/null @@ -1,16 +0,0 @@ -resources: -- type: compute.v1.instance - name: vm-created-by-cloud-config-{{ properties["deployment"] }} - properties: - zone: {{ properties["zone"] }} - machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-{{ properties["deployment"] }} - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default diff --git a/cmd/expandybird/test/templates/jinja_defaults.jinja.schema b/cmd/expandybird/test/templates/jinja_defaults.jinja.schema deleted file mode 100644 index 7cf4ae108..000000000 --- a/cmd/expandybird/test/templates/jinja_defaults.jinja.schema +++ /dev/null @@ -1,18 +0,0 @@ -info: - title: Schema for a basic jinja template that includes default values - -imports: - -properties: - foo: - description: blah - type: string - zone: - type: string - default: test-zone - project: - type: string - default: test-project - deployment: - type: string - default: test-deployment diff --git a/cmd/expandybird/test/templates/jinja_defaults.yaml b/cmd/expandybird/test/templates/jinja_defaults.yaml deleted file mode 100644 index 1b3ee64f4..000000000 --- a/cmd/expandybird/test/templates/jinja_defaults.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: -- path: "jinja_defaults.jinja" -- path: "jinja_defaults.jinja.schema" - -resources: -- name: jinja_defaults_name - type: jinja_defaults.jinja - properties: - foo: bar diff --git a/cmd/expandybird/test/templates/jinja_defaults_result.yaml b/cmd/expandybird/test/templates/jinja_defaults_result.yaml deleted file mode 100644 index 60c03b408..000000000 --- a/cmd/expandybird/test/templates/jinja_defaults_result.yaml +++ /dev/null @@ -1,29 +0,0 @@ -config: - resources: - - name: vm-created-by-cloud-config-test-deployment - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - zone: test-zone - type: compute.v1.instance -layout: - resources: - - name: jinja_defaults_name - properties: - deployment: test-deployment - foo: bar - project: test-project - zone: test-zone - resources: - - name: vm-created-by-cloud-config-test-deployment - type: compute.v1.instance - type: jinja_defaults.jinja diff --git a/cmd/expandybird/test/templates/jinja_missing_required.jinja b/cmd/expandybird/test/templates/jinja_missing_required.jinja deleted file mode 100644 index 2247bf2e1..000000000 --- a/cmd/expandybird/test/templates/jinja_missing_required.jinja +++ /dev/null @@ -1,4 +0,0 @@ -Nothing here because this file should never be called. -The validation will fail before this file is used. - -{{% Invalid diff --git a/cmd/expandybird/test/templates/jinja_missing_required.jinja.schema b/cmd/expandybird/test/templates/jinja_missing_required.jinja.schema deleted file mode 100644 index 387b64dde..000000000 --- a/cmd/expandybird/test/templates/jinja_missing_required.jinja.schema +++ /dev/null @@ -1,11 +0,0 @@ -info: - title: Schema with a required property - -imports: - -required: - - important -properties: - important: - type: string - diff --git a/cmd/expandybird/test/templates/jinja_missing_required.yaml b/cmd/expandybird/test/templates/jinja_missing_required.yaml deleted file mode 100644 index 412bccf8e..000000000 --- a/cmd/expandybird/test/templates/jinja_missing_required.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: -- path: "jinja_missing_required.jinja" -- path: "jinja_missing_required.jinja.schema" - -resources: -- name: jinja_missing_required_resource_name - type: jinja_missing_required.jinja - properties: - less-important: an optional property diff --git a/cmd/expandybird/test/templates/jinja_multiple_errors.jinja b/cmd/expandybird/test/templates/jinja_multiple_errors.jinja deleted file mode 100644 index 2247bf2e1..000000000 --- a/cmd/expandybird/test/templates/jinja_multiple_errors.jinja +++ /dev/null @@ -1,4 +0,0 @@ -Nothing here because this file should never be called. -The validation will fail before this file is used. - -{{% Invalid diff --git a/cmd/expandybird/test/templates/jinja_multiple_errors.jinja.schema b/cmd/expandybird/test/templates/jinja_multiple_errors.jinja.schema deleted file mode 100644 index 5d73e125d..000000000 --- a/cmd/expandybird/test/templates/jinja_multiple_errors.jinja.schema +++ /dev/null @@ -1,22 +0,0 @@ -info: - title: Schema with several rules - -imports: - -properties: - number: - type: integer - short-string: - type: string - maxLength: 10 - odd: - type: integer - not: - multipleOf: 2 - abc: - type: string - enum: - - a - - b - - c - diff --git a/cmd/expandybird/test/templates/jinja_multiple_errors.yaml b/cmd/expandybird/test/templates/jinja_multiple_errors.yaml deleted file mode 100644 index 0fc663628..000000000 --- a/cmd/expandybird/test/templates/jinja_multiple_errors.yaml +++ /dev/null @@ -1,12 +0,0 @@ -imports: -- path: "jinja_multiple_errors.jinja" -- path: "jinja_multiple_errors.jinja.schema" - -resources: -- name: jinja_multiple_errors - type: jinja_multiple_errors.jinja - properties: - number: a string - short-string: longer than 10 chars - odd: 6 - abc: d diff --git a/cmd/expandybird/test/templates/jinja_noparams.jinja b/cmd/expandybird/test/templates/jinja_noparams.jinja deleted file mode 100644 index 9b0ec9476..000000000 --- a/cmd/expandybird/test/templates/jinja_noparams.jinja +++ /dev/null @@ -1,19 +0,0 @@ -resources: -{% for name in ['name1', 'name2'] %} -- type: compute.v1.instance - name: {{ name }} - properties: - zone: test-zone - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default -{% endfor %} - diff --git a/cmd/expandybird/test/templates/jinja_noparams.yaml b/cmd/expandybird/test/templates/jinja_noparams.yaml deleted file mode 100644 index 7debc85e2..000000000 --- a/cmd/expandybird/test/templates/jinja_noparams.yaml +++ /dev/null @@ -1,6 +0,0 @@ -imports: ["jinja_noparams.jinja"] - -resources: -- name: jinja_noparams_name - type: jinja_noparams.jinja - diff --git a/cmd/expandybird/test/templates/jinja_noparams_result.yaml b/cmd/expandybird/test/templates/jinja_noparams_result.yaml deleted file mode 100644 index 5a5f0e52c..000000000 --- a/cmd/expandybird/test/templates/jinja_noparams_result.yaml +++ /dev/null @@ -1,41 +0,0 @@ -config: - resources: - - name: name1 - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - zone: test-zone - type: compute.v1.instance - - name: name2 - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - zone: test-zone - type: compute.v1.instance -layout: - resources: - - name: jinja_noparams_name - resources: - - name: name1 - type: compute.v1.instance - - name: name2 - type: compute.v1.instance - type: jinja_noparams.jinja diff --git a/cmd/expandybird/test/templates/jinja_template.jinja b/cmd/expandybird/test/templates/jinja_template.jinja deleted file mode 100644 index 5febf21bd..000000000 --- a/cmd/expandybird/test/templates/jinja_template.jinja +++ /dev/null @@ -1,18 +0,0 @@ -resources: -- type: compute.v1.instance - name: vm-created-by-cloud-config-{{ properties["deployment"] }} - properties: - zone: {{ properties["zone"] }} - machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-{{ properties["deployment"] }} - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default - - diff --git a/cmd/expandybird/test/templates/jinja_template.yaml b/cmd/expandybird/test/templates/jinja_template.yaml deleted file mode 100644 index bc2b2b0db..000000000 --- a/cmd/expandybird/test/templates/jinja_template.yaml +++ /dev/null @@ -1,10 +0,0 @@ -imports: ["jinja_template.jinja"] - -resources: -- name: jinja_template_name - type: jinja_template.jinja - properties: - zone: test-zone - project: test-project - deployment: test-deployment - diff --git a/cmd/expandybird/test/templates/jinja_template_result.yaml b/cmd/expandybird/test/templates/jinja_template_result.yaml deleted file mode 100644 index 10a36fa56..000000000 --- a/cmd/expandybird/test/templates/jinja_template_result.yaml +++ /dev/null @@ -1,28 +0,0 @@ -config: - resources: - - name: vm-created-by-cloud-config-test-deployment - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - zone: test-zone - type: compute.v1.instance -layout: - resources: - - name: jinja_template_name - properties: - deployment: test-deployment - project: test-project - zone: test-zone - resources: - - name: vm-created-by-cloud-config-test-deployment - type: compute.v1.instance - type: jinja_template.jinja diff --git a/cmd/expandybird/test/templates/jinja_template_with_env.jinja b/cmd/expandybird/test/templates/jinja_template_with_env.jinja deleted file mode 100644 index 545824e5e..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_env.jinja +++ /dev/null @@ -1,18 +0,0 @@ -resources: -- type: compute.v1.instance - name: vm-created-by-cloud-config-{{ env["deployment"] }} - properties: - zone: {{ properties["zone"] }} - machineType: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-{{ env["deployment"] }}-{{ env["name"] }}-{{ env["type"] }} - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/global/networks/default - - diff --git a/cmd/expandybird/test/templates/jinja_template_with_env.yaml b/cmd/expandybird/test/templates/jinja_template_with_env.yaml deleted file mode 100644 index 58388635c..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_env.yaml +++ /dev/null @@ -1,8 +0,0 @@ -imports: ["jinja_template_with_env.jinja"] - -resources: -- name: jinja_template_with_env_name - type: jinja_template_with_env.jinja - properties: - zone: test-zone - diff --git a/cmd/expandybird/test/templates/jinja_template_with_env_result.yaml b/cmd/expandybird/test/templates/jinja_template_with_env_result.yaml deleted file mode 100644 index a69f89ab1..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_env_result.yaml +++ /dev/null @@ -1,26 +0,0 @@ -config: - resources: - - name: vm-created-by-cloud-config-test-deployment - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment-jinja_template_with_env_name-jinja_template_with_env.jinja - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - zone: test-zone - type: compute.v1.instance -layout: - resources: - - name: jinja_template_with_env_name - properties: - zone: test-zone - resources: - - name: vm-created-by-cloud-config-test-deployment - type: compute.v1.instance - type: jinja_template_with_env.jinja diff --git a/cmd/expandybird/test/templates/jinja_template_with_import.jinja b/cmd/expandybird/test/templates/jinja_template_with_import.jinja deleted file mode 100644 index b5c125726..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_import.jinja +++ /dev/null @@ -1,6 +0,0 @@ -{% import 'helpers/common.jinja' as common %} -resources: -- name: {{ common.GenerateMachineName("myFrontend", "prod") }} - type: compute.v1.instance - properties: - machineSize: big diff --git a/cmd/expandybird/test/templates/jinja_template_with_import.yaml b/cmd/expandybird/test/templates/jinja_template_with_import.yaml deleted file mode 100644 index d4ec9f327..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_import.yaml +++ /dev/null @@ -1,5 +0,0 @@ -imports: ["jinja_template_with_import.jinja", "helpers/common.jinja"] - -resources: -- name: jinja_template_with_import_name - type: jinja_template_with_import.jinja diff --git a/cmd/expandybird/test/templates/jinja_template_with_import_result.yaml b/cmd/expandybird/test/templates/jinja_template_with_import_result.yaml deleted file mode 100644 index 3a3c3b2e5..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_import_result.yaml +++ /dev/null @@ -1,13 +0,0 @@ -config: - resources: - - name: myFrontend-prod - properties: - machineSize: big - type: compute.v1.instance -layout: - resources: - - name: jinja_template_with_import_name - resources: - - name: myFrontend-prod - type: compute.v1.instance - type: jinja_template_with_import.jinja diff --git a/cmd/expandybird/test/templates/jinja_template_with_inlinedfile.jinja b/cmd/expandybird/test/templates/jinja_template_with_inlinedfile.jinja deleted file mode 100644 index 01d7642e4..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_inlinedfile.jinja +++ /dev/null @@ -1,7 +0,0 @@ -{% import 'helpers/common.jinja' as common %} -resources: -- name: {{ common.GenerateMachineName("myFrontend", "prod") }} - type: compute.v1.instance - properties: - description: {{ imports[properties["description-file"]] }} - machineSize: big diff --git a/cmd/expandybird/test/templates/jinja_template_with_inlinedfile.yaml b/cmd/expandybird/test/templates/jinja_template_with_inlinedfile.yaml deleted file mode 100644 index e8bec0891..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_inlinedfile.yaml +++ /dev/null @@ -1,7 +0,0 @@ -imports: ["jinja_template_with_inlinedfile.jinja", "helpers/common.jinja", "description_text.txt"] - -resources: -- name: jinja_template_with_inlinedfile_name - type: jinja_template_with_inlinedfile.jinja - properties: - description-file: description_text.txt diff --git a/cmd/expandybird/test/templates/jinja_template_with_inlinedfile_result.yaml b/cmd/expandybird/test/templates/jinja_template_with_inlinedfile_result.yaml deleted file mode 100644 index 6f4bf9eee..000000000 --- a/cmd/expandybird/test/templates/jinja_template_with_inlinedfile_result.yaml +++ /dev/null @@ -1,21 +0,0 @@ -config: - resources: - - name: myFrontend-prod - properties: - description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim - veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse - cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat - non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - machineSize: big - type: compute.v1.instance -layout: - resources: - - name: jinja_template_with_inlinedfile_name - properties: - description-file: description_text.txt - resources: - - name: myFrontend-prod - type: compute.v1.instance - type: jinja_template_with_inlinedfile.jinja diff --git a/cmd/expandybird/test/templates/jinja_unresolved.jinja b/cmd/expandybird/test/templates/jinja_unresolved.jinja deleted file mode 100644 index 6ad1ed1cd..000000000 --- a/cmd/expandybird/test/templates/jinja_unresolved.jinja +++ /dev/null @@ -1,18 +0,0 @@ -resources: -- type: compute.v1.instance - name: vm-created-by-cloud-config-{{ porcelain["deployment"] }} - properties: - zone: {{ properties["zone"] }} - machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-{{ properties["deployment"] }} - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default - - diff --git a/cmd/expandybird/test/templates/jinja_unresolved.yaml b/cmd/expandybird/test/templates/jinja_unresolved.yaml deleted file mode 100644 index 8bc31af11..000000000 --- a/cmd/expandybird/test/templates/jinja_unresolved.yaml +++ /dev/null @@ -1,10 +0,0 @@ -imports: ["jinja_unresolved.jinja"] - -resources: -- name: jinja_template_name - type: jinja_unresolved.jinja - properties: - zone: test-zone - project: test-project - deployment: test-deployment - diff --git a/cmd/expandybird/test/templates/no_properties.py b/cmd/expandybird/test/templates/no_properties.py deleted file mode 100644 index 66cd164c5..000000000 --- a/cmd/expandybird/test/templates/no_properties.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Return empty resources block.""" - - -def GenerateConfig(_): - return """resources:""" diff --git a/cmd/expandybird/test/templates/no_properties.yaml b/cmd/expandybird/test/templates/no_properties.yaml deleted file mode 100644 index 1c7d29795..000000000 --- a/cmd/expandybird/test/templates/no_properties.yaml +++ /dev/null @@ -1,6 +0,0 @@ -imports: -- path: "no_properties.py" - -resources: -- name: test-resource - type: no_properties.py diff --git a/cmd/expandybird/test/templates/no_properties_result.yaml b/cmd/expandybird/test/templates/no_properties_result.yaml deleted file mode 100644 index 41ccb5602..000000000 --- a/cmd/expandybird/test/templates/no_properties_result.yaml +++ /dev/null @@ -1,6 +0,0 @@ -config: - resources: [] -layout: - resources: - - name: test-resource - type: no_properties.py diff --git a/cmd/expandybird/test/templates/no_resources.py b/cmd/expandybird/test/templates/no_resources.py deleted file mode 100644 index c387ebca0..000000000 --- a/cmd/expandybird/test/templates/no_resources.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Does nothing.""" - - -def GenerateConfig(_): - """Returns empty string.""" - return '' diff --git a/cmd/expandybird/test/templates/no_resources.yaml b/cmd/expandybird/test/templates/no_resources.yaml deleted file mode 100644 index d9257d8ea..000000000 --- a/cmd/expandybird/test/templates/no_resources.yaml +++ /dev/null @@ -1,6 +0,0 @@ -imports: -- path: "no_resources.py" - -resources: -- name: test-resource - type: no_resources.py diff --git a/cmd/expandybird/test/templates/python_and_jinja_template.jinja b/cmd/expandybird/test/templates/python_and_jinja_template.jinja deleted file mode 100644 index 8a670b476..000000000 --- a/cmd/expandybird/test/templates/python_and_jinja_template.jinja +++ /dev/null @@ -1,17 +0,0 @@ -resources: -- type: compute.v1.instance - name: vm-created-by-cloud-config-{{ properties["deployment"] }} - properties: - zone: {{ properties["zone"] }} - machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-{{ properties["deployment"] }} - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default - diff --git a/cmd/expandybird/test/templates/python_and_jinja_template.py b/cmd/expandybird/test/templates/python_and_jinja_template.py deleted file mode 100644 index 86562c9d0..000000000 --- a/cmd/expandybird/test/templates/python_and_jinja_template.py +++ /dev/null @@ -1,35 +0,0 @@ -#% description: Creates a VM running a Salt master daemon in a Docker container. -#% parameters: -#% - name: masterAddress -#% type: string -#% description: Name of the Salt master VM. -#% required: true -#% - name: project -#% type: string -#% description: Name of the Cloud project. -#% required: true -#% - name: zone -#% type: string -#% description: Zone to create the resources in. -#% required: true - -"""Generates config for a VM running a SaltStack master. - -Just for fun this template is in Python, while the others in this -directory are in Jinja2. -""" - - -def GenerateConfig(evaluation_context): - return """ -resources: -- name: python_and_jinja_template_jinja_name - type: python_and_jinja_template.jinja - properties: - zone: %(zone)s - project: %(project)s - deployment: %(master)s - -""" % {"master": evaluation_context.properties["masterAddress"], - "project": evaluation_context.properties["project"], - "zone": evaluation_context.properties["zone"]} diff --git a/cmd/expandybird/test/templates/python_and_jinja_template.yaml b/cmd/expandybird/test/templates/python_and_jinja_template.yaml deleted file mode 100644 index 46daafc27..000000000 --- a/cmd/expandybird/test/templates/python_and_jinja_template.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: ["python_and_jinja_template.jinja", "python_and_jinja_template.py"] - -resources: -- name: python_and_jinja_template_name - type: python_and_jinja_template.py - properties: - masterAddress: master-address - project: my-project - zone: my-zone diff --git a/cmd/expandybird/test/templates/python_and_jinja_template_result.yaml b/cmd/expandybird/test/templates/python_and_jinja_template_result.yaml deleted file mode 100644 index 3d23dcfbf..000000000 --- a/cmd/expandybird/test/templates/python_and_jinja_template_result.yaml +++ /dev/null @@ -1,35 +0,0 @@ -config: - resources: - - name: vm-created-by-cloud-config-master-address - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-master-address - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - zone: my-zone - type: compute.v1.instance -layout: - resources: - - name: python_and_jinja_template_name - properties: - masterAddress: master-address - project: my-project - zone: my-zone - resources: - - name: python_and_jinja_template_jinja_name - properties: - deployment: master-address - project: my-project - zone: my-zone - resources: - - name: vm-created-by-cloud-config-master-address - type: compute.v1.instance - type: python_and_jinja_template.jinja - type: python_and_jinja_template.py diff --git a/cmd/expandybird/test/templates/python_bad_schema.py b/cmd/expandybird/test/templates/python_bad_schema.py deleted file mode 100644 index 0ebda1f6c..000000000 --- a/cmd/expandybird/test/templates/python_bad_schema.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Throws an exception.""" - -raise Exception diff --git a/cmd/expandybird/test/templates/python_bad_schema.py.schema b/cmd/expandybird/test/templates/python_bad_schema.py.schema deleted file mode 100644 index cf5d5d6d0..000000000 --- a/cmd/expandybird/test/templates/python_bad_schema.py.schema +++ /dev/null @@ -1,19 +0,0 @@ -info: - title: Schema with several errors - -imports: - -properties: - bad-type: - type: int - missing-cond: - type: string - exclusiveMaximum: 10 - odd-string: - type: string - not: - multipleOf: 2 - bad-enum: - type: string - enum: not a list - diff --git a/cmd/expandybird/test/templates/python_bad_schema.yaml b/cmd/expandybird/test/templates/python_bad_schema.yaml deleted file mode 100644 index d52024b6f..000000000 --- a/cmd/expandybird/test/templates/python_bad_schema.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: -- path: "python_bad_schema.py" -- path: "python_bad_schema.py.schema" - -resources: -- name: python_bad_schema - type: python_bad_schema.py - properties: - innocent: true diff --git a/cmd/expandybird/test/templates/python_noparams.py b/cmd/expandybird/test/templates/python_noparams.py deleted file mode 100644 index 2542d71bb..000000000 --- a/cmd/expandybird/test/templates/python_noparams.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Constructs a VM.""" - - -def GenerateConfig(_): - """Generates config of a VM.""" - return """ -resources: -- name: myBackend - type: compute.v1.instance - properties: - machineSize: big -""" diff --git a/cmd/expandybird/test/templates/python_noparams.yaml b/cmd/expandybird/test/templates/python_noparams.yaml deleted file mode 100644 index b7abeaf55..000000000 --- a/cmd/expandybird/test/templates/python_noparams.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: ["python_noparams.py"] - -resources: -- name: myFrontend - type: compute.v1.instance - properties: - machineSize: big -- name: python_noparams_name - type: python_noparams.py diff --git a/cmd/expandybird/test/templates/python_noparams_result.yaml b/cmd/expandybird/test/templates/python_noparams_result.yaml deleted file mode 100644 index 944d9018a..000000000 --- a/cmd/expandybird/test/templates/python_noparams_result.yaml +++ /dev/null @@ -1,19 +0,0 @@ -config: - resources: - - name: myFrontend - properties: - machineSize: big - type: compute.v1.instance - - name: myBackend - properties: - machineSize: big - type: compute.v1.instance -layout: - resources: - - name: myFrontend - type: compute.v1.instance - - name: python_noparams_name - resources: - - name: myBackend - type: compute.v1.instance - type: python_noparams.py diff --git a/cmd/expandybird/test/templates/python_schema.py b/cmd/expandybird/test/templates/python_schema.py deleted file mode 100644 index 2d935f7ad..000000000 --- a/cmd/expandybird/test/templates/python_schema.py +++ /dev/null @@ -1,57 +0,0 @@ -#% description: Creates a VM running a Salt master daemon in a Docker container. -#% parameters: -#% - name: masterAddress -#% type: string -#% description: Name of the Salt master VM. -#% required: true -#% - name: project -#% type: string -#% description: Name of the Cloud project. -#% required: true -#% - name: zone -#% type: string -#% description: Zone to create the resources in. -#% required: true - -"""Generates config for a VM running a SaltStack master. - -Just for fun this template is in Python, while the others in this -directory are in Jinja2. -""" - - -def GenerateConfig(evaluation_context): - return """ -resources: -- type: compute.v1.firewall - name: %(master)s-firewall - properties: - network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default - sourceRanges: [ "0.0.0.0/0" ] - allowed: - - IPProtocol: tcp - ports: [ "4505", "4506" ] -- type: compute.v1.instance - name: %(master)s - properties: - zone: %(zone)s - machineType: https://www.googleapis.com/compute/v1/projects/%(project)s/zones/%(zone)s/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default - accessConfigs: - - name: External NAT - type: ONE_TO_ONE_NAT - metadata: - items: - - key: startup-script - value: startup-script-value -""" % {"master": evaluation_context.properties["masterAddress"], - "project": evaluation_context.env["project"], - "zone": evaluation_context.properties["zone"]} diff --git a/cmd/expandybird/test/templates/python_schema.py.schema b/cmd/expandybird/test/templates/python_schema.py.schema deleted file mode 100644 index cb960bcfd..000000000 --- a/cmd/expandybird/test/templates/python_schema.py.schema +++ /dev/null @@ -1,14 +0,0 @@ -info: - title: A simple python template that has a schema. - -imports: - -properties: - masterAddress: - type: string - default: slave-address - description: masterAddress - zone: - type: string - default: not-test-zone - description: zone diff --git a/cmd/expandybird/test/templates/python_schema.yaml b/cmd/expandybird/test/templates/python_schema.yaml deleted file mode 100644 index 94f94d4e5..000000000 --- a/cmd/expandybird/test/templates/python_schema.yaml +++ /dev/null @@ -1,10 +0,0 @@ -imports: -- path: "python_schema.py" -- path: "python_schema.py.schema" - -resources: -- name: python_schema - type: python_schema.py - properties: - masterAddress: master-address - zone: my-zone diff --git a/cmd/expandybird/test/templates/python_schema_result.yaml b/cmd/expandybird/test/templates/python_schema_result.yaml deleted file mode 100644 index 2b75d97c2..000000000 --- a/cmd/expandybird/test/templates/python_schema_result.yaml +++ /dev/null @@ -1,46 +0,0 @@ -config: - resources: - - name: master-address-firewall - properties: - allowed: - - IPProtocol: tcp - ports: - - '4505' - - '4506' - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - sourceRanges: - - 0.0.0.0/0 - type: compute.v1.firewall - - name: master-address - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro - metadata: - items: - - key: startup-script - value: startup-script-value - networkInterfaces: - - accessConfigs: - - name: External NAT - type: ONE_TO_ONE_NAT - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - zone: my-zone - type: compute.v1.instance -layout: - resources: - - name: python_schema - properties: - masterAddress: master-address - zone: my-zone - resources: - - name: master-address-firewall - type: compute.v1.firewall - - name: master-address - type: compute.v1.instance - type: python_schema.py diff --git a/cmd/expandybird/test/templates/python_template.py b/cmd/expandybird/test/templates/python_template.py deleted file mode 100644 index 57ff7fe73..000000000 --- a/cmd/expandybird/test/templates/python_template.py +++ /dev/null @@ -1,57 +0,0 @@ -#% description: Creates a VM running a Salt master daemon in a Docker container. -#% parameters: -#% - name: masterAddress -#% type: string -#% description: Name of the Salt master VM. -#% required: true -#% - name: project -#% type: string -#% description: Name of the Cloud project. -#% required: true -#% - name: zone -#% type: string -#% description: Zone to create the resources in. -#% required: true - -"""Generates config for a VM running a SaltStack master. - -Just for fun this template is in Python, while the others in this -directory are in Jinja2. -""" - - -def GenerateConfig(evaluation_context): - return """ -resources: -- type: compute.v1.firewall - name: %(master)s-firewall - properties: - network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default - sourceRanges: [ "0.0.0.0/0" ] - allowed: - - IPProtocol: tcp - ports: [ "4505", "4506" ] -- type: compute.v1.instance - name: %(master)s - properties: - zone: %(zone)s - machineType: https://www.googleapis.com/compute/v1/projects/%(project)s/zones/%(zone)s/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default - accessConfigs: - - name: External NAT - type: ONE_TO_ONE_NAT - metadata: - items: - - key: startup-script - value: startup-script-value -""" % {"master": evaluation_context.properties["masterAddress"], - "project": evaluation_context.properties["project"], - "zone": evaluation_context.properties["zone"]} diff --git a/cmd/expandybird/test/templates/python_template.yaml b/cmd/expandybird/test/templates/python_template.yaml deleted file mode 100644 index 08ae8b8bb..000000000 --- a/cmd/expandybird/test/templates/python_template.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: ["python_template.py"] - -resources: -- name: python_template_name - type: python_template.py - properties: - masterAddress: master-address - project: my-project - zone: my-zone diff --git a/cmd/expandybird/test/templates/python_template_result.yaml b/cmd/expandybird/test/templates/python_template_result.yaml deleted file mode 100644 index 1b82f3fed..000000000 --- a/cmd/expandybird/test/templates/python_template_result.yaml +++ /dev/null @@ -1,47 +0,0 @@ -config: - resources: - - name: master-address-firewall - properties: - allowed: - - IPProtocol: tcp - ports: - - '4505' - - '4506' - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - sourceRanges: - - 0.0.0.0/0 - type: compute.v1.firewall - - name: master-address - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro - metadata: - items: - - key: startup-script - value: startup-script-value - networkInterfaces: - - accessConfigs: - - name: External NAT - type: ONE_TO_ONE_NAT - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - zone: my-zone - type: compute.v1.instance -layout: - resources: - - name: python_template_name - properties: - masterAddress: master-address - project: my-project - zone: my-zone - resources: - - name: master-address-firewall - type: compute.v1.firewall - - name: master-address - type: compute.v1.instance - type: python_template.py diff --git a/cmd/expandybird/test/templates/python_template_with_env.py b/cmd/expandybird/test/templates/python_template_with_env.py deleted file mode 100644 index 2d935f7ad..000000000 --- a/cmd/expandybird/test/templates/python_template_with_env.py +++ /dev/null @@ -1,57 +0,0 @@ -#% description: Creates a VM running a Salt master daemon in a Docker container. -#% parameters: -#% - name: masterAddress -#% type: string -#% description: Name of the Salt master VM. -#% required: true -#% - name: project -#% type: string -#% description: Name of the Cloud project. -#% required: true -#% - name: zone -#% type: string -#% description: Zone to create the resources in. -#% required: true - -"""Generates config for a VM running a SaltStack master. - -Just for fun this template is in Python, while the others in this -directory are in Jinja2. -""" - - -def GenerateConfig(evaluation_context): - return """ -resources: -- type: compute.v1.firewall - name: %(master)s-firewall - properties: - network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default - sourceRanges: [ "0.0.0.0/0" ] - allowed: - - IPProtocol: tcp - ports: [ "4505", "4506" ] -- type: compute.v1.instance - name: %(master)s - properties: - zone: %(zone)s - machineType: https://www.googleapis.com/compute/v1/projects/%(project)s/zones/%(zone)s/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default - accessConfigs: - - name: External NAT - type: ONE_TO_ONE_NAT - metadata: - items: - - key: startup-script - value: startup-script-value -""" % {"master": evaluation_context.properties["masterAddress"], - "project": evaluation_context.env["project"], - "zone": evaluation_context.properties["zone"]} diff --git a/cmd/expandybird/test/templates/python_template_with_env.yaml b/cmd/expandybird/test/templates/python_template_with_env.yaml deleted file mode 100644 index d3bbc26c7..000000000 --- a/cmd/expandybird/test/templates/python_template_with_env.yaml +++ /dev/null @@ -1,8 +0,0 @@ -imports: ["python_template_with_env.py"] - -resources: -- name: python_template_with_env_name - type: python_template_with_env.py - properties: - masterAddress: master-address - zone: my-zone diff --git a/cmd/expandybird/test/templates/python_template_with_env_result.yaml b/cmd/expandybird/test/templates/python_template_with_env_result.yaml deleted file mode 100644 index 027732c8f..000000000 --- a/cmd/expandybird/test/templates/python_template_with_env_result.yaml +++ /dev/null @@ -1,46 +0,0 @@ -config: - resources: - - name: master-address-firewall - properties: - allowed: - - IPProtocol: tcp - ports: - - '4505' - - '4506' - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - sourceRanges: - - 0.0.0.0/0 - type: compute.v1.firewall - - name: master-address - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro - metadata: - items: - - key: startup-script - value: startup-script-value - networkInterfaces: - - accessConfigs: - - name: External NAT - type: ONE_TO_ONE_NAT - network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default - zone: my-zone - type: compute.v1.instance -layout: - resources: - - name: python_template_with_env_name - properties: - masterAddress: master-address - zone: my-zone - resources: - - name: master-address-firewall - type: compute.v1.firewall - - name: master-address - type: compute.v1.instance - type: python_template_with_env.py diff --git a/cmd/expandybird/test/templates/python_template_with_import.py b/cmd/expandybird/test/templates/python_template_with_import.py deleted file mode 100644 index ba362233a..000000000 --- a/cmd/expandybird/test/templates/python_template_with_import.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Constructs a VM.""" -import json - -import helpers.common -import helpers.extra.common2 - - -def GenerateConfig(_): - """Generates config of a VM.""" - return """ -resources: -- name: %s - type: compute.v1.instance - properties: - machineSize: %s -""" % (helpers.common.GenerateMachineName( - json.dumps('myFrontend').strip('"'), 'prod'), - helpers.extra.common2.GenerateMachineSize()) diff --git a/cmd/expandybird/test/templates/python_template_with_import.yaml b/cmd/expandybird/test/templates/python_template_with_import.yaml deleted file mode 100644 index 73cce18f1..000000000 --- a/cmd/expandybird/test/templates/python_template_with_import.yaml +++ /dev/null @@ -1,5 +0,0 @@ -imports: ["python_template_with_import.py", "helpers/common.py", "helpers/common2.py", "helpers/__init__.py"] - -resources: -- name: python_template_with_import_name - type: python_template_with_import.py diff --git a/cmd/expandybird/test/templates/python_template_with_import_result.yaml b/cmd/expandybird/test/templates/python_template_with_import_result.yaml deleted file mode 100644 index d8e283308..000000000 --- a/cmd/expandybird/test/templates/python_template_with_import_result.yaml +++ /dev/null @@ -1,13 +0,0 @@ -config: - resources: - - name: myFrontend-prod - properties: - machineSize: big - type: compute.v1.instance -layout: - resources: - - name: python_template_with_import_name - resources: - - name: myFrontend-prod - type: compute.v1.instance - type: python_template_with_import.py diff --git a/cmd/expandybird/test/templates/python_template_with_inlinedfile.py b/cmd/expandybird/test/templates/python_template_with_inlinedfile.py deleted file mode 100644 index 2ea1fb8ed..000000000 --- a/cmd/expandybird/test/templates/python_template_with_inlinedfile.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Constructs a VM.""" - -# Verify that both ways of hierarchical imports work. -from helpers import common -import helpers.extra.common2 - - -def GenerateConfig(evaluation_context): - """Generates config of a VM.""" - return """ -resources: -- name: %s - type: compute.v1.instance - properties: - description: %s - machineSize: %s -""" % (common.GenerateMachineName("myFrontend", "prod"), - evaluation_context.imports[ - evaluation_context.properties["description-file"]], - helpers.extra.common2.GenerateMachineSize()) diff --git a/cmd/expandybird/test/templates/python_template_with_inlinedfile.yaml b/cmd/expandybird/test/templates/python_template_with_inlinedfile.yaml deleted file mode 100644 index 8c1d8c38c..000000000 --- a/cmd/expandybird/test/templates/python_template_with_inlinedfile.yaml +++ /dev/null @@ -1,7 +0,0 @@ -imports: ["python_template_with_inlinedfile.py", "helpers/common.py", "helpers/common2.py", "helpers/__init__.py", "description_text.txt"] - -resources: -- name: python_template_with_inlinedfile_name - type: python_template_with_inlinedfile.py - properties: - description-file: description_text.txt diff --git a/cmd/expandybird/test/templates/python_template_with_inlinedfile_result.yaml b/cmd/expandybird/test/templates/python_template_with_inlinedfile_result.yaml deleted file mode 100644 index 92706a0fd..000000000 --- a/cmd/expandybird/test/templates/python_template_with_inlinedfile_result.yaml +++ /dev/null @@ -1,21 +0,0 @@ -config: - resources: - - name: myFrontend-prod - properties: - description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim - veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse - cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat - non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - machineSize: big - type: compute.v1.instance -layout: - resources: - - name: python_template_with_inlinedfile_name - properties: - description-file: description_text.txt - resources: - - name: myFrontend-prod - type: compute.v1.instance - type: python_template_with_inlinedfile.py diff --git a/cmd/expandybird/test/templates/python_with_exception.py b/cmd/expandybird/test/templates/python_with_exception.py deleted file mode 100644 index bf982a82f..000000000 --- a/cmd/expandybird/test/templates/python_with_exception.py +++ /dev/null @@ -1,7 +0,0 @@ -"""A python script that raise exceptions. - -""" - - -def GenerateConfig(unused_context): - raise NameError('No file found') diff --git a/cmd/expandybird/test/templates/python_with_exception.yaml b/cmd/expandybird/test/templates/python_with_exception.yaml deleted file mode 100644 index fadc23970..000000000 --- a/cmd/expandybird/test/templates/python_with_exception.yaml +++ /dev/null @@ -1,9 +0,0 @@ -imports: ["python_with_exception.py"] - -resources: -- name: python_with_exception_name - type: python_with_exception.py - properties: - masterAddress: master-address - project: my-project - zone: my-zone diff --git a/cmd/expandybird/test/templates/simple.yaml b/cmd/expandybird/test/templates/simple.yaml deleted file mode 100644 index 5065fa564..000000000 --- a/cmd/expandybird/test/templates/simple.yaml +++ /dev/null @@ -1,18 +0,0 @@ -resources: -- type: compute.v1.instance - name: vm-created-by-cloud-config-{{ params["deployment"] }} - properties: - zone: test-zone - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - disks: - - deviceName: boot - type: PERSISTENT - boot: true - autoDelete: true - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - - diff --git a/cmd/expandybird/test/templates/simple_result.yaml b/cmd/expandybird/test/templates/simple_result.yaml deleted file mode 100644 index 4353c7194..000000000 --- a/cmd/expandybird/test/templates/simple_result.yaml +++ /dev/null @@ -1,21 +0,0 @@ -config: - resources: - - name: vm-created-by-cloud-config-{{ params["deployment"] }} - properties: - disks: - - autoDelete: true - boot: true - deviceName: boot - initializeParams: - diskName: disk-created-by-cloud-config-test-deployment - sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619 - type: PERSISTENT - machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro - networkInterfaces: - - network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default - zone: test-zone - type: compute.v1.instance -layout: - resources: - - name: vm-created-by-cloud-config-{{ params["deployment"] }} - type: compute.v1.instance diff --git a/cmd/expandybird/test/templates/use_helper.jinja b/cmd/expandybird/test/templates/use_helper.jinja deleted file mode 100644 index c748a8ae4..000000000 --- a/cmd/expandybird/test/templates/use_helper.jinja +++ /dev/null @@ -1,7 +0,0 @@ -resources: -- name: use-helper - type: foo - properties: - test: {{ properties['barfoo'] }} -- name: use-helper-helper - type: helper.jinja diff --git a/cmd/expandybird/test/templates/use_helper.jinja.schema b/cmd/expandybird/test/templates/use_helper.jinja.schema deleted file mode 100644 index 69986603d..000000000 --- a/cmd/expandybird/test/templates/use_helper.jinja.schema +++ /dev/null @@ -1,4 +0,0 @@ -properties: - barfoo: - type: string - default: Use this schema also diff --git a/cmd/expandybird/test/templates/use_helper.yaml b/cmd/expandybird/test/templates/use_helper.yaml deleted file mode 100644 index 818b0d284..000000000 --- a/cmd/expandybird/test/templates/use_helper.yaml +++ /dev/null @@ -1,3 +0,0 @@ -resources: -- name: use-helper - type: use_helper.jinja diff --git a/cmd/expandybird/test/templates/use_helper_result.yaml b/cmd/expandybird/test/templates/use_helper_result.yaml deleted file mode 100644 index 51c17afc8..000000000 --- a/cmd/expandybird/test/templates/use_helper_result.yaml +++ /dev/null @@ -1,26 +0,0 @@ -config: - resources: - - name: use-helper - properties: - test: Use this schema also - type: foo - - name: helper - properties: - test: Use this schema - type: bar -layout: - resources: - - name: use-helper - properties: - barfoo: Use this schema also - resources: - - name: use-helper - type: foo - - name: use-helper-helper - properties: - foobar: Use this schema - resources: - - name: helper - type: bar - type: helper.jinja - type: use_helper.jinja diff --git a/cmd/goexpander/expander/expander.go b/cmd/goexpander/expander/expander.go deleted file mode 100644 index ee7c513f3..000000000 --- a/cmd/goexpander/expander/expander.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expander - -import ( - "bytes" - "fmt" - "io" - "strings" - "text/template" - - "github.com/Masterminds/sprig" - "github.com/cloudfoundry-incubator/candiedyaml" - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/expansion" -) - -// parseYAMLStream takes an encoded YAML stream and turns it into a slice of JSON-marshalable -// objects, one for each document in the stream. -func parseYAMLStream(in io.Reader) ([]interface{}, error) { - // Use candiedyaml because it's the only one that supports streams. - decoder := candiedyaml.NewDecoder(in) - var document interface{} - stream := []interface{}{} - for { - err := decoder.Decode(&document) - if err != nil { - if strings.Contains(err.Error(), "Expected document start at line") { - return stream, nil - } - return nil, err - } - // Now it's held in document but we have to do a bit of a dance to get it in a form that can - // be marshaled as JSON for our API response. The fundamental problem is that YAML is a - // superset of JSON in that it can represent non-string keys, and full IEEE floating point - // values (NaN etc). JSON only allows string keys and its definition of a number is based - // around a sequence of digits. - - // Kubernetes does not make use of these features, as it uses YAML as just "pretty JSON". - // Consequently this does not affect Helm either. However, both candiedyaml and go-yaml - // return types that are too wide for JSON marshalling (i.e. map[interface{}]interface{} - // instead of map[string]interface{}), so we have to do some explicit conversion. Luckily, - // ghodss/yaml has code to help with this, since decoding from YAML to JSON-marshalable - // values is exactly the problem that it was designed to solve. - - // 1) Marshal it back to YAML string. - yamlBytes, err := candiedyaml.Marshal(document) - if err != nil { - return nil, err - } - - // 2) Use ghodss/yaml to unmarshal that string into JSON-compatible data structures. - var jsonObj interface{} - if err := yaml.Unmarshal(yamlBytes, &jsonObj); err != nil { - return nil, err - } - - // Now it's suitable for embedding in an API response. - stream = append(stream, jsonObj) - } -} - -type expander struct { -} - -// NewExpander returns an Go Templating expander. -func NewExpander() expansion.Expander { - return &expander{} -} - -// ExpandChart resolves the given files to a sequence of JSON-marshalable values. -func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) { - - err := expansion.ValidateRequest(request) - if err != nil { - return nil, err - } - - request, err = expansion.ValidateProperties(request) - if err != nil { - return nil, err - } - - chartInv := request.ChartInvocation - chartMembers := request.Chart.Members - - resources := []interface{}{} - for _, file := range chartMembers { - name := file.Path - content := file.Content - tmpl := template.New(name).Funcs(sprig.HermeticTxtFuncMap()) - - for _, otherFile := range chartMembers { - otherName := otherFile.Path - otherContent := otherFile.Content - if name == otherName { - continue - } - _, err := tmpl.Parse(string(otherContent)) - if err != nil { - return nil, err - } - } - - // Have to put something in that resolves non-empty or Go templates get confused. - _, err := tmpl.Parse("# Content begins now") - if err != nil { - return nil, err - } - - tmpl, err = tmpl.Parse(string(content)) - if err != nil { - return nil, err - } - - generated := bytes.NewBuffer(nil) - if err := tmpl.ExecuteTemplate(generated, name, chartInv.Properties); err != nil { - return nil, err - } - - stream, err := parseYAMLStream(generated) - if err != nil { - return nil, fmt.Errorf("%s\nContent:\n%s", err.Error(), generated) - } - - for _, doc := range stream { - resources = append(resources, doc) - } - } - - return &expansion.ServiceResponse{Resources: resources}, nil -} diff --git a/cmd/goexpander/expander/expander_test.go b/cmd/goexpander/expander/expander_test.go deleted file mode 100644 index 95ce37c9d..000000000 --- a/cmd/goexpander/expander/expander_test.go +++ /dev/null @@ -1,262 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expander - -import ( - "fmt" - "reflect" - "strings" - "testing" - - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/expansion" -) - -// content provides an easy way to provide file content verbatim in tests. -func content(lines []string) []byte { - return []byte(strings.Join(lines, "\n") + "\n") -} - -func testExpansion(t *testing.T, req *expansion.ServiceRequest, - expResponse *expansion.ServiceResponse, expError string) { - backend := NewExpander() - response, err := backend.ExpandChart(req) - if err != nil { - message := err.Error() - if expResponse != nil || !strings.Contains(message, expError) { - t.Fatalf("unexpected error: %s\n", message) - } - } else { - if expResponse == nil { - t.Fatalf("expected error did not occur: %s\n", expError) - } - if !reflect.DeepEqual(response, expResponse) { - message := fmt.Sprintf( - "want:\n%s\nhave:\n%s\n", expResponse, response) - t.Fatalf("output mismatch:\n%s\n", message) - } - } -} - -var goExpander = &chart.Expander{ - Name: "GoTemplating", - Entrypoint: "templates/main.py", -} - -func TestEmpty(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz", - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: "Test", - Expander: goExpander, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{}, - }, - "", // Error - ) -} - -func TestSingle(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz", - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: "Test", - Expander: goExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.yaml", - Content: content([]string{ - "name: foo", - "type: bar", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "type": "bar", - }, - }, - }, - "", // Error - ) -} - -func TestProperties(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz", - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: "Test", - Expander: goExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/main.yaml", - Content: content([]string{ - "name: foo", - "type: {{ .prop2 }}", - "properties:", - " something: {{ .prop1 }}", - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "foo", - "properties": map[string]interface{}{ - "something": 3.0, - }, - "type": "foo", - }, - }, - }, - "", // Error - ) -} - -func TestComplex(t *testing.T) { - testExpansion( - t, - &expansion.ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz", - Properties: map[string]interface{}{ - "DatabaseName": "mydb", - "NumRepicas": 3, - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: "Test", - Expander: goExpander, - }, - Members: []*chart.Member{ - { - Path: "templates/bar.tmpl", - Content: content([]string{ - `{{ template "banana" . }}`, - }), - }, - { - Path: "templates/base.tmpl", - Content: content([]string{ - `{{ define "apple" }}`, - `name: Abby`, - `kind: Apple`, - `dbname: {{default "whatdb" .DatabaseName}}`, - `{{ end }}`, - ``, - `{{ define "banana" }}`, - `name: Bobby`, - `kind: Banana`, - `dbname: {{default "whatdb" .DatabaseName}}`, - `{{ end }}`, - }), - }, - { - Path: "templates/foo.tmpl", - Content: content([]string{ - `---`, - `foo:`, - ` bar: baz`, - `---`, - `{{ template "apple" . }}`, - `---`, - `{{ template "apple" . }}`, - `...`, - }), - }, - { - Path: "templates/docs.txt", - Content: content([]string{ - `{{/*`, - `File contains only a comment.`, - `Suitable for documentation within templates/`, - `*/}}`, - }), - }, - { - Path: "templates/docs2.txt", - Content: content([]string{ - `# File contains only a comment.`, - `# Suitable for documentation within templates/`, - }), - }, - }, - }, - }, - &expansion.ServiceResponse{ - Resources: []interface{}{ - map[string]interface{}{ - "name": "Bobby", - "kind": "Banana", - "dbname": "mydb", - }, - map[string]interface{}{ - "foo": map[string]interface{}{ - "bar": "baz", - }, - }, - map[string]interface{}{ - "name": "Abby", - "kind": "Apple", - "dbname": "mydb", - }, - map[string]interface{}{ - "name": "Abby", - "kind": "Apple", - "dbname": "mydb", - }, - }, - }, - "", // Error - ) -} diff --git a/cmd/goexpander/main.go b/cmd/goexpander/main.go deleted file mode 100644 index ded71a35c..000000000 --- a/cmd/goexpander/main.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "github.com/kubernetes/helm/cmd/goexpander/expander" - "github.com/kubernetes/helm/pkg/expansion" - "github.com/kubernetes/helm/pkg/version" - - "flag" - "log" -) - -// interface that we are going to listen on -var address = flag.String("address", "", "Interface to listen on") - -// port that we are going to listen on -var port = flag.Int("port", 8080, "Port to listen on") - -func main() { - flag.Parse() - backend := expander.NewExpander() - service := expansion.NewService(*address, *port, backend) - log.Printf("Version: %s", version.Version) - log.Printf("Listening on http://%s:%d/expand", *address, *port) - log.Fatal(service.ListenAndServe()) -} diff --git a/cmd/helm/Makefile b/cmd/helm/Makefile deleted file mode 100644 index 30cfef5a5..000000000 --- a/cmd/helm/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -SHELL := /bin/bash - -GOLANG_CROSSPLATFORMS := darwin/386 darwin/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm windows/386 windows/amd64 - -ROOT_DIR := $(abspath ./../..) -BIN_DIR := $(ROOT_DIR)/bin - -DEFAULT_BUCKET := gs://get-helm -STORAGE_BUCKET ?= $(DEFAULT_BUCKET) - -DEFAULT_TAG := git-$(shell git rev-parse --short HEAD) -TAG ?= $(DEFAULT_TAG) - -all: push - -push: - for platform in ${GOLANG_CROSSPLATFORMS}; do \ - echo $$platform; \ - PLATFORM=$${platform%/*} && ARCH=$${platform##*/} && \ - BINARY=$${PLATFORM}-$${ARCH} && \ - ZIP=${TAG}-helm-$${BINARY}.zip && \ - zip -j $${ZIP} ${BIN_DIR}/$${BINARY}/helm* && \ - gsutil cp $${ZIP} ${STORAGE_BUCKET} && \ - rm $${ZIP} ; \ - done diff --git a/cmd/helm/chart.go b/cmd/helm/chart.go deleted file mode 100644 index 91d4e7e85..000000000 --- a/cmd/helm/chart.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/format" -) - -func init() { - addCommands(chartCommands()) -} - -const chartDesc = `A Chart is a package that can be installed and managed by Helm. - - The 'helm chart' subcommands provide tools for working with Helm charts. To - get started creating your own chart, use 'helm chart create NAME'. - - For details, use 'helm chart CMD -h'. -` - -func chartCommands() cli.Command { - return cli.Command{ - // Names following form prescribed here: http://is.gd/QUSEOF - Name: "chart", - Usage: "Perform chart-centered operations.", - Description: chartDesc, - Subcommands: []cli.Command{ - { - Name: "create", - Usage: "Create a new chart directory and set up base files and directories.", - ArgsUsage: "CHARTNAME", - Action: func(c *cli.Context) { run(c, createChart) }, - }, - { - Name: "package", - Aliases: []string{"pack"}, - Usage: "Given a chart directory, package it into a release.", - ArgsUsage: "PATH", - Action: func(c *cli.Context) { run(c, pack) }, - }, - }, - } -} - -func createChart(c *cli.Context) error { - args := c.Args() - if len(args) < 1 { - return errors.New("'helm create' requires a chart name as an argument") - } - - dir, name := filepath.Split(args[0]) - - cf := &chart.Chartfile{ - Name: name, - Description: "Created by Helm", - Version: "0.1.0", - } - - _, err := chart.Create(cf, dir) - return err - -} - -func pack(cxt *cli.Context) error { - args := cxt.Args() - if len(args) < 1 { - return errors.New("'helm package' requires a path to a chart directory as an argument") - } - - dir := args[0] - if fi, err := os.Stat(dir); err != nil { - return fmt.Errorf("Could not find directory %s: %s", dir, err) - } else if !fi.IsDir() { - return fmt.Errorf("Not a directory: %s", dir) - } - - fname, err := packDir(dir) - if err != nil { - return err - } - format.Msg(fname) - return nil -} - -func packDir(dir string) (string, error) { - c, err := chart.LoadDir(dir) - if err != nil { - return "", fmt.Errorf("Failed to load %s: %s", dir, err) - } - - return chart.Save(c, ".") -} diff --git a/cmd/helm/chart_upload.go b/cmd/helm/chart_upload.go deleted file mode 100644 index 6077675d3..000000000 --- a/cmd/helm/chart_upload.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "fmt" - "os" - "regexp" - "strings" - - "github.com/aokoli/goutils" - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/format" -) - -func uploadChart(c *cli.Context) error { - args := c.Args() - if len(args) < 1 { - format.Err("First argument, filename, is required. Try 'helm deploy --help'") - os.Exit(1) - } - - cname := c.String("name") - fname := args[0] - - if fname == "" { - return errors.New("A filename must be specified. For a tar archive, this is the name of the root template in the archive.") - } - - _, err := doUpload(fname, cname, c) - return err -} -func doUpload(filename, cname string, cxt *cli.Context) (string, error) { - - fi, err := os.Stat(filename) - if err != nil { - return "", err - } - - if fi.IsDir() { - format.Info("Chart is directory") - c, err := chart.LoadDir(filename) - if err != nil { - return "", err - } - if cname == "" { - cname = genName(c.Chartfile().Name) - } - - // TODO: Is it better to generate the file in temp dir like this, or - // just put it in the CWD? - //tdir, err := ioutil.TempDir("", "helm-") - //if err != nil { - //format.Warn("Could not create temporary directory. Using .") - //tdir = "." - //} else { - //defer os.RemoveAll(tdir) - //} - tdir := "." - tfile, err := chart.Save(c, tdir) - if err != nil { - return "", err - } - filename = tfile - } else if cname == "" { - n, _, e := parseTarName(filename) - if e != nil { - return "", e - } - cname = n - } - - // TODO: Add a version build metadata on the chart. - - if cxt.Bool("dry-run") { - format.Info("Prepared deploy %q using file %q", cname, filename) - return "", nil - } - - c := NewClient(cxt) - return c.PostChart(filename, cname) -} - -func genName(pname string) string { - s, _ := goutils.RandomAlphaNumeric(8) - return fmt.Sprintf("%s-%s", pname, s) -} - -func parseTarName(name string) (string, string, error) { - tnregexp := regexp.MustCompile(chart.TarNameRegex) - if strings.HasSuffix(name, ".tgz") { - name = strings.TrimSuffix(name, ".tgz") - } - v := tnregexp.FindStringSubmatch(name) - if v == nil { - return name, "", fmt.Errorf("invalid name %s", name) - } - return v[1], v[2], nil -} diff --git a/cmd/helm/deploy.go b/cmd/helm/deploy.go deleted file mode 100644 index 5db207bc6..000000000 --- a/cmd/helm/deploy.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "fmt" - "io/ioutil" - "os" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/common" - "gopkg.in/yaml.v2" -) - -func init() { - addCommands(deployCmd()) -} - -func deployCmd() cli.Command { - return cli.Command{ - Name: "deploy", - Usage: "Deploy a chart into the cluster.", - ArgsUsage: "[CHART]", - Action: func(c *cli.Context) { run(c, deploy) }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config,c", - Usage: "The configuration YAML file for this deployment.", - }, - cli.StringFlag{ - Name: "name,n", - Usage: "Name of deployment, used for deploy and update commands (defaults to template name)", - }, - // TODO: I think there is a Generic flag type that we can implement parsing with. - cli.StringFlag{ - Name: "properties,p", - Usage: "A comma-separated list of key=value pairs: 'foo=bar,foo2=baz'.", - }, - }, - } -} - -func deploy(c *cli.Context) error { - - res := &common.Resource{ - // By default - Properties: map[string]interface{}{}, - } - - if c.String("config") != "" { - // If there is a configuration file, use it. - err := loadConfig(c.String("config"), &res.Properties) - if err != nil { - return err - } - } - - args := c.Args() - if len(args) == 0 { - return fmt.Errorf("Need chart name on commandline") - } - res.Type = args[0] - - if name := c.String("name"); len(name) > 0 { - res.Name = name - } else { - return fmt.Errorf("Need deployed name on commandline") - } - - if props, err := parseProperties(c.String("properties")); err != nil { - return err - } else if len(props) > 0 { - // Coalesce the properties into the first props. We have no way of - // knowing which resource the properties are supposed to be part - // of. - for n, v := range props { - res.Properties[n] = v - } - } - - return NewClient(c).PostDeployment(res) -} - -// isLocalChart returns true if the given path can be statted. -func isLocalChart(path string) bool { - _, err := os.Stat(path) - return err == nil -} - -// loadConfig loads chart arguments into c -func loadConfig(filename string, dest *map[string]interface{}) error { - data, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - return yaml.Unmarshal(data, dest) -} diff --git a/cmd/helm/deployment.go b/cmd/helm/deployment.go deleted file mode 100644 index c17b68023..000000000 --- a/cmd/helm/deployment.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "os" - "regexp" - "text/template" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/format" -) - -var ( - errMissingDeploymentArg = errors.New("First argument, deployment name, is required. Try 'helm get --help'") - errTooManyArgs = errors.New("Too many arguments provided. Try 'helm dep describe [DEPLOYMENT]'") -) - -const deploymentDesc = `A deployment is an instance of a Chart running in the cluster. - - Deployments have a name, a chart, and possibly a set of properites. The deployment - commands provide tools for managing deployments. - - To deploy a new chart, use the top-level 'helm deploy' command. From there, - the 'helm deployment' commands may be used to work with the deployed - application. - - For more help, use 'helm deployment CMD -h'.` - -const defaultShowFormat = `Name: {{.Name}} -Status: {{.State.Status}} -{{with .State.Errors}}Errors: -{{range .}} {{.}}{{end}} -{{end}}` - -const defaultShowResourceFormat = `Name: {{.Name}} -Type: {{.Type}} -Status: {{.State.Status}} -{{with .State.Errors}}Errors: -{{range .}} {{.}}{{end}} -{{end}}` - -func init() { - addCommands(deploymentCommands()) -} - -func deploymentCommands() cli.Command { - return cli.Command{ - // Names following form prescribed here: http://is.gd/QUSEOF - Name: "deployment", - Aliases: []string{"dep"}, - Usage: "Perform deployment-centered operations.", - Description: deploymentDesc, - Subcommands: []cli.Command{ - { - Name: "remove", - Aliases: []string{"rm"}, - Usage: "Deletes the named deployment(s).", - ArgsUsage: "DEPLOYMENT [DEPLOYMENT [...]]", - Action: func(c *cli.Context) { run(c, deleteDeployment) }, - }, - { - Name: "describe", - Usage: "Describes the kubernetes resources for the named deployment(s).", - ArgsUsage: "DEPLOYMENT", - Action: func(c *cli.Context) { run(c, describeDeployment) }, - }, - { - Name: "show", - Aliases: []string{"info"}, - Usage: "Provide details about this deployment.", - ArgsUsage: "", - Action: func(c *cli.Context) { run(c, showDeployment) }, - }, - { - Name: "list", - Aliases: []string{"ls"}, - Usage: "list all deployments, or filter by an optional regular expression.", - ArgsUsage: "REGEXP", - Action: func(c *cli.Context) { run(c, listDeployments) }, - }, - }, - } -} - -func listDeployments(c *cli.Context) error { - list, err := NewClient(c).ListDeployments() - if err != nil { - return err - } - args := c.Args() - if len(args) >= 1 { - pattern := args[0] - r, err := regexp.Compile(pattern) - if err != nil { - return err - } - - newlist := []string{} - for _, i := range list { - if r.MatchString(i) { - newlist = append(newlist, i) - } - } - list = newlist - } - - if len(list) == 0 { - return errors.New("no deployments found") - } - - format.List(list) - return nil -} - -func deleteDeployment(c *cli.Context) error { - args := c.Args() - if len(args) < 1 { - return errMissingDeploymentArg - } - for _, name := range args { - deployment, err := NewClient(c).DeleteDeployment(name) - if err != nil { - return err - } - format.Info("Deleted %q at %s", name, deployment.DeletedAt) - } - return nil -} - -func describeDeployment(c *cli.Context) error { - args := c.Args() - if len(args) < 1 { - return errMissingDeploymentArg - } - if len(args) > 1 { - return errTooManyArgs - } - name := args[0] - manifest, err := NewClient(c).DescribeDeployment(name) - if err != nil { - return err - } - - if manifest.ExpandedConfig == nil { - return errors.New("No ExpandedConfig found for: " + name) - } - - for _, resource := range manifest.ExpandedConfig.Resources { - tmpl := template.Must(template.New("showresource").Parse(defaultShowResourceFormat)) - err = tmpl.Execute(os.Stdout, resource) - if err != nil { - return err - } - - } - return nil -} - -func showDeployment(c *cli.Context) error { - args := c.Args() - if len(args) < 1 { - return errMissingDeploymentArg - } - name := args[0] - deployment, err := NewClient(c).GetDeployment(name) - if err != nil { - return err - } - tmpl := template.Must(template.New("show").Parse(defaultShowFormat)) - return tmpl.Execute(os.Stdout, deployment) -} diff --git a/cmd/helm/deployment_test.go b/cmd/helm/deployment_test.go deleted file mode 100644 index 581cbc6aa..000000000 --- a/cmd/helm/deployment_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "encoding/json" - "net/http" - "testing" - - "github.com/kubernetes/helm/pkg/common" -) - -type pathAndResponse struct { - path string - resp interface{} -} - -func TestDeployment(t *testing.T) { - var deploymentTestCases = []struct { - args []string - resp []pathAndResponse - expected string - }{ - { - []string{"deployment", "show", "guestbook.yaml"}, - []pathAndResponse{{"/deployments/", &common.Deployment{ - Name: "guestbook.yaml", - State: &common.DeploymentState{Status: common.CreatedStatus}, - }}}, - "Name: guestbook.yaml\nStatus: Created\n", - }, - { - []string{"deployment", "show", "guestbook.yaml"}, - []pathAndResponse{{"/deployments/", &common.Deployment{ - Name: "guestbook.yaml", - State: &common.DeploymentState{ - Status: common.FailedStatus, - Errors: []string{"error message"}, - }, - }}}, - "Name: guestbook.yaml\nStatus: Failed\nErrors:\n error message\n", - }, - { - []string{"deployment", "list"}, - []pathAndResponse{{"/deployments/", []string{"guestbook.yaml"}}}, - "guestbook.yaml\n", - }, - { - []string{"deployment", "describe", "guestbook.yaml"}, - []pathAndResponse{{ - "/deployments/guestbook.yaml", - &common.Deployment{Name: "guestbook.yaml", - State: &common.DeploymentState{Status: common.CreatedStatus}, - LatestManifest: "manifestxyz", - }}, - {"/deployments/guestbook.yaml/manifests/manifestxyz", &common.Manifest{ - Deployment: "guestbook.yaml", - Name: "manifestxyz", - ExpandedConfig: &common.Configuration{ - Resources: []*common.Resource{ - {Name: "fe-rc", Type: "ReplicationController", State: &common.ResourceState{Status: common.Created}}, - {Name: "fe", Type: "Service", State: &common.ResourceState{Status: common.Created}}, - {Name: "be-rc", Type: "ReplicationController", State: &common.ResourceState{Status: common.Created}}, - {Name: "be", Type: "Service", State: &common.ResourceState{Status: common.Created}}, - }, - }, - }}}, - "Name: fe-rc\nType: ReplicationController\nStatus: Created\n" + - "Name: fe\nType: Service\nStatus: Created\n" + - "Name: be-rc\nType: ReplicationController\nStatus: Created\n" + - "Name: be\nType: Service\nStatus: Created\n", - }, - } - - for _, tc := range deploymentTestCases { - th := testHelm(t) - for _, pathAndResponse := range tc.resp { - var response = pathAndResponse.resp - th.mux.HandleFunc(pathAndResponse.path, func(w http.ResponseWriter, r *http.Request) { - data, err := json.Marshal(response) - th.must(err) - w.Write(data) - }) - } - - th.run(tc.args...) - - if tc.expected != th.output { - t.Errorf("Expected %v got %v", tc.expected, th.output) - } - th.cleanup() - } -} diff --git a/cmd/helm/doctor.go b/cmd/helm/doctor.go deleted file mode 100644 index 5fb8c1e43..000000000 --- a/cmd/helm/doctor.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/client" - "github.com/kubernetes/helm/pkg/format" - "github.com/kubernetes/helm/pkg/kubectl" -) - -func init() { - addCommands(doctorCmd()) -} - -func doctorCmd() cli.Command { - return cli.Command{ - Name: "doctor", - Usage: "Run a series of checks for necessary prerequisites.", - ArgsUsage: "", - Action: func(c *cli.Context) { run(c, doctor) }, - } -} - -func doctor(c *cli.Context) error { - var runner kubectl.Runner - runner = &kubectl.RealRunner{} - if client.IsInstalled(runner) { - format.Success("You have everything you need. Go forth my friend!") - } else { - format.Warning("Looks like you don't have the helm server-side components installed.\nRun: `helm server install`") - } - - return nil -} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go deleted file mode 100644 index 94b529671..000000000 --- a/cmd/helm/helm.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "os" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/client" - "github.com/kubernetes/helm/pkg/format" - "github.com/kubernetes/helm/pkg/version" -) - -const desc = `Helm: the package and deployment manager for Kubernetes - - Helm is a tool for packaging, deploying, and managing Kubernetes - applications. It has a client component (this tool) and several in-cluster - components. - - Before you can use Helm to manage applications, you must install the - in-cluster components into the target Kubernetes cluster: - - $ helm server install - - Once the in-cluster portion is running, you can use 'helm deploy' to - deploy a new application: - - $ helm deploy -n NAME CHART - - For more information on Helm commands, you can use the following tools: - - $ helm help # top-level help - $ helm CMD --help # help for a particular command or set of commands -` - -var commands []cli.Command - -func init() { - addCommands(cmds()...) -} - -// debug indicates whether the process is in debug mode. -// -// This is set at app start-up time, based on the presence of the --debug -// flag. -var debug bool - -func main() { - app := cli.NewApp() - app.Name = "helm" - app.Version = version.Version - app.Usage = desc - app.Commands = commands - - // TODO: make better - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "host,u", - Usage: "The URL of the DM server", - EnvVar: "HELM_HOST", - Value: "https://localhost:8000/", - }, - cli.StringFlag{ - Name: "kubectl", - Usage: "The path to the kubectl binary", - EnvVar: "KUBECTL", - }, - cli.IntFlag{ - Name: "timeout", - Usage: "Time in seconds to wait for response", - Value: 10, - }, - cli.BoolFlag{ - Name: "debug", - Usage: "Enable verbose debugging output", - }, - } - app.Before = func(c *cli.Context) error { - debug = c.GlobalBool("debug") - return nil - } - app.Run(os.Args) -} - -func cmds() []cli.Command { - return []cli.Command{} -} - -func addCommands(cmds ...cli.Command) { - commands = append(commands, cmds...) -} - -func run(c *cli.Context, f func(c *cli.Context) error) { - if err := f(c); err != nil { - format.Err(err) - os.Exit(1) - } -} - -// NewClient creates a new client instance preconfigured for CLI usage. -func NewClient(c *cli.Context) *client.Client { - host := c.GlobalString("host") - timeout := c.GlobalInt("timeout") - return client.NewClient(host).SetDebug(debug).SetTimeout(timeout) -} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go deleted file mode 100644 index 481be8a85..000000000 --- a/cmd/helm/helm_test.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/format" -) - -type testHelmData struct { - t *testing.T - mux *http.ServeMux - server *httptest.Server - app *cli.App - output string -} - -func testHelm(t *testing.T) *testHelmData { - th := &testHelmData{t: t} - - th.app = cli.NewApp() - th.app.Commands = commands - - th.app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "host,u", - Value: "https://localhost:8000/", - }, - cli.IntFlag{ - Name: "timeout", - Value: 10, - }, - cli.BoolFlag{ - Name: "debug", - }, - } - th.app.Before = func(c *cli.Context) error { - debug = c.GlobalBool("debug") - return nil - } - - th.mux = http.NewServeMux() - th.server = httptest.NewServer(th.mux) - - return th -} - -func (th *testHelmData) cleanup() { - th.server.Close() -} - -func (th *testHelmData) URL() string { - return th.server.URL -} - -// must gives a fatal error if err is not nil. -func (th *testHelmData) must(err error) { - if err != nil { - th.t.Fatal(err) - } -} - -// check gives a test non-fatal error if err is not nil. -func (th *testHelmData) check(err error) { - if err != nil { - th.t.Error(err) - } -} - -func (th *testHelmData) run(args ...string) { - th.output = "" - args = append([]string{"helm", "--host", th.URL()}, args...) - th.output = captureOutput(func() { - th.app.Run(args) - }) -} - -// captureOutput redirect all log/std streams, capture and replace -func captureOutput(fn func()) string { - osStdout, osStderr := os.Stdout, os.Stderr - logStdout, logStderr := format.Stdout, format.Stderr - defer func() { - os.Stdout, os.Stderr = osStdout, osStderr - format.Stdout, format.Stderr = logStdout, logStderr - }() - - r, w, _ := os.Pipe() - - os.Stdout, os.Stderr = w, w - format.Stdout, format.Stderr = w, w - - fn() - - // read test output and restore previous stdout - w.Close() - b, _ := ioutil.ReadAll(r) - return string(b) -} diff --git a/cmd/helm/properties.go b/cmd/helm/properties.go deleted file mode 100644 index df8797f78..000000000 --- a/cmd/helm/properties.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "strconv" - "strings" -) - -// TODO: The concept of property here is really simple. We could definitely get -// better about the values we allow. Also, we need some validation on the names. - -var errInvalidProperty = errors.New("property is not in name=value format") - -// parseProperties is a utility for parsing a comma-separated key=value string. -func parseProperties(kvstr string) (map[string]interface{}, error) { - properties := map[string]interface{}{} - - if len(kvstr) == 0 { - return properties, nil - } - - pairs := strings.Split(kvstr, ",") - for _, p := range pairs { - // Allow for "k=v, k=v" - p = strings.TrimSpace(p) - pair := strings.Split(p, "=") - if len(pair) < 2 { - return properties, errInvalidProperty - } - - // If the value looks int-like, convert it. - if i, err := strconv.Atoi(pair[1]); err == nil { - properties[pair[0]] = i - } else { - properties[pair[0]] = pair[1] - } - } - - return properties, nil -} diff --git a/cmd/helm/repository.go b/cmd/helm/repository.go deleted file mode 100644 index f17c2fae2..000000000 --- a/cmd/helm/repository.go +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "encoding/json" - "errors" - "path/filepath" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/format" - "github.com/kubernetes/helm/pkg/repo" -) - -func init() { - addCommands(repoCommands()) -} - -const chartRepoPath = "repositories" - -const repoDesc = `Helm repositories store Helm charts. - - The repository commands are used to manage which Helm repositories Helm may - use as a source for Charts. The repositories are accessed by in-cluster Helm - components. - - To list the repositories that your server knows about, use 'helm repo list'. - - For more details, use 'helm repo CMD -h'. -` - -const addRepoDesc = `The add repository command is used to add a name a repository url to your - chart repository list. The repository url must begin with a valid protocoal. At the moment, - we only support google cloud storage for chart repositories. - - A valid command might look like: - $ helm repo add charts gs://kubernetes-charts -` - -func repoCommands() cli.Command { - return cli.Command{ - Name: "repository", - Aliases: []string{"repo"}, - Usage: "Perform chart repository operations.", - Description: repoDesc, - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "Add a chart repository to the remote manager.", - Description: addRepoDesc, - ArgsUsage: "[NAME] [REPOSITORY_URL]", - Action: func(c *cli.Context) { run(c, addRepo) }, - }, - { - Name: "list", - Usage: "List the chart repositories on the remote manager.", - ArgsUsage: "", - Action: func(c *cli.Context) { run(c, listRepos) }, - }, - { - Name: "remove", - Aliases: []string{"rm"}, - Usage: "Remove a chart repository from the remote manager.", - ArgsUsage: "REPOSITORY_NAME", - Action: func(c *cli.Context) { run(c, removeRepo) }, - }, - }, - } -} - -func addRepo(c *cli.Context) error { - args := c.Args() - if len(args) < 2 { - return errors.New("'helm repo add' requires a name and repository url as arguments") - } - name := args[0] - repoURL := args[1] - payload, _ := json.Marshal(repo.Repo{URL: repoURL, Name: name}) - msg := "" - if _, err := NewClient(c).Post(chartRepoPath, payload, &msg); err != nil { - return err - } - format.Info(name + " has been added to your chart repositories!") - return nil -} - -func listRepos(c *cli.Context) error { - dest := map[string]string{} - if _, err := NewClient(c).Get(chartRepoPath, &dest); err != nil { - return err - } - if len(dest) < 1 { - format.Info("Looks like you don't have any chart repositories.") - format.Info("Add a chart repository using the `helm repo add [REPOSITORY_URL]` command.") - } else { - format.Msg("Chart Repositories:\n") - for k, v := range dest { - //TODO: make formatting pretty - format.Msg(k + "\t" + v + "\n") - } - } - return nil -} - -func removeRepo(c *cli.Context) error { - args := c.Args() - if len(args) < 1 { - return errors.New("'helm repo remove' requires a repository name as an argument") - } - name := args[0] - if _, err := NewClient(c).Delete(filepath.Join(chartRepoPath, name), nil); err != nil { - return err - } - format.Msg(name + " has been removed.\n") - return nil -} diff --git a/cmd/helm/server.go b/cmd/helm/server.go deleted file mode 100644 index ee902da40..000000000 --- a/cmd/helm/server.go +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "fmt" - - "github.com/codegangsta/cli" - "github.com/kubernetes/helm/pkg/client" - "github.com/kubernetes/helm/pkg/format" - "github.com/kubernetes/helm/pkg/kubectl" -) - -// ErrAlreadyInstalled indicates that Helm Server is already installed. -var ErrAlreadyInstalled = errors.New("Already Installed") - -func init() { - addCommands(dmCmd()) -} - -func dmCmd() cli.Command { - return cli.Command{ - Name: "server", - Usage: "Manage Helm server-side components", - Description: `Server commands manage the in-cluster portion of Helm. - - Helm has several components that run inside of Kubernetes. Before Helm can - be used to install and manage packages, it must be installed into the - Kubernetes cluster in which packages will be installed. - - The 'helm server' commands rely upon a properly configured 'kubectl' to - communicate with the Kubernetes cluster. To verify that your 'kubectl' - client is pointed to the correct cluster, use 'kubectl cluster-info'. - - Use 'helm server install' to install the in-cluster portion of Helm. -`, - Subcommands: []cli.Command{ - { - Name: "install", - Usage: "Install Helm server components on Kubernetes.", - ArgsUsage: "", - Description: `Use kubectl to install Helm components in their own namespace on Kubernetes. - - Make sure your Kubernetes environment is pointed to the cluster on which you - wish to install.`, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "dry-run", - Usage: "Show what would be installed, but don't install anything.", - }, - cli.StringFlag{ - Name: "resourcifier-image", - Usage: "The full image name of the Docker image for resourcifier.", - EnvVar: "HELM_RESOURCIFIER_IMAGE", - }, - cli.StringFlag{ - Name: "expandybird-image", - Usage: "The full image name of the Docker image for expandybird.", - EnvVar: "HELM_EXPANDYBIRD_IMAGE", - }, - cli.StringFlag{ - Name: "manager-image", - Usage: "The full image name of the Docker image for manager.", - EnvVar: "HELM_MANAGER_IMAGE", - }, - }, - Action: func(c *cli.Context) { run(c, installServer) }, - }, - { - Name: "uninstall", - Usage: "Uninstall the Helm server-side from Kubernetes.", - ArgsUsage: "", - Description: ``, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "dry-run", - Usage: "Show what would be uninstalled, but don't remove anything.", - }, - }, - Action: func(c *cli.Context) { run(c, uninstallServer) }, - }, - { - Name: "status", - Usage: "Show status of Helm server-side components.", - ArgsUsage: "", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "dry-run", - Usage: "Only display the underlying kubectl commands.", - }, - }, - Action: func(c *cli.Context) { run(c, statusServer) }, - }, - { - Name: "target", - Usage: "Displays information about the Kubernetes cluster.", - ArgsUsage: "", - Action: func(c *cli.Context) { run(c, targetServer) }, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "dry-run", - Usage: "Only display the underlying kubectl commands.", - }, - }, - }, - }, - } -} - -func installServer(c *cli.Context) error { - resImg := c.String("resourcifier-image") - ebImg := c.String("expandybird-image") - manImg := c.String("manager-image") - - dryRun := c.Bool("dry-run") - kubectlPath := c.GlobalString("kubectl") - runner := buildKubectlRunner(kubectlPath, dryRun) - - i := client.NewInstaller() - i.Manager["Image"] = manImg - i.Resourcifier["Image"] = resImg - i.Expandybird["Image"] = ebImg - - out, err := i.Install(runner) - if err != nil { - return fmt.Errorf("error installing %s %s", string(out), err) - } - format.Msg(out) - return nil -} - -func uninstallServer(c *cli.Context) error { - dryRun := c.Bool("dry-run") - kubectlPath := c.GlobalString("kubectl") - runner := buildKubectlRunner(kubectlPath, dryRun) - - out, err := client.Uninstall(runner) - if err != nil { - return fmt.Errorf("error uninstalling: %s %s", out, err) - } - format.Msg(out) - return nil -} - -func statusServer(c *cli.Context) error { - dryRun := c.Bool("dry-run") - kubectlPath := c.GlobalString("kubectl") - runner := buildKubectlRunner(kubectlPath, dryRun) - - out, err := runner.GetByKind("pods", "", "helm") - if err != nil { - return err - } - format.Msg(string(out)) - return nil -} - -func targetServer(c *cli.Context) error { - dryRun := c.Bool("dry-run") - kubectlPath := c.GlobalString("kubectl") - runner := buildKubectlRunner(kubectlPath, dryRun) - - out, err := runner.ClusterInfo() - if err != nil { - return fmt.Errorf("%s (%s)", out, err) - } - format.Msg(string(out)) - return nil -} - -func buildKubectlRunner(kubectlPath string, dryRun bool) kubectl.Runner { - if dryRun { - return &kubectl.PrintRunner{} - } - // TODO: Refactor out kubectl.Path global - if kubectlPath != "" { - kubectl.Path = kubectlPath - } - return &kubectl.RealRunner{} -} diff --git a/cmd/manager/chartrepos.go b/cmd/manager/chartrepos.go deleted file mode 100644 index 8a1ab1304..000000000 --- a/cmd/manager/chartrepos.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "github.com/kubernetes/helm/cmd/manager/router" - "github.com/kubernetes/helm/pkg/httputil" - "github.com/kubernetes/helm/pkg/repo" - "github.com/kubernetes/helm/pkg/util" - - "encoding/json" - "net/http" - "net/url" - "regexp" -) - -func registerChartRepoRoutes(c *router.Context, h *router.Handler) { - h.Add("GET /repositories", listChartReposHandlerFunc) - h.Add("GET /repositories/*", getChartRepoHandlerFunc) - h.Add("GET /repositories/*/charts", listRepoChartsHandlerFunc) - h.Add("GET /repositories/*/charts/*", getRepoChartHandlerFunc) - h.Add("POST /repositories", addChartRepoHandlerFunc) - h.Add("DELETE /repositories/*", removeChartRepoHandlerFunc) -} - -func listChartReposHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: list chart repositories" - repos, err := c.Manager.ListRepos() - if err != nil { - return err - } - - util.LogHandlerExitWithJSON(handler, w, repos, http.StatusOK) - return nil -} - -func addChartRepoHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: add chart repository" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - cr := &repo.Repo{} - if err := httputil.Decode(w, r, cr); err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - if string(cr.Format) == "" { - cr.Format = repo.GCSRepoFormat - } - - if string(cr.Type) == "" { - cr.Type = repo.GCSRepoType - } - - if err := c.Manager.AddRepo(cr); err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - msg, _ := json.Marshal(cr.Name + " has been added to the list of chart repositories.") - util.LogHandlerExitWithJSON(handler, w, msg, http.StatusCreated) - return nil -} - -func removeChartRepoHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: remove chart repository" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - name, err := pos(w, r, 2) - if err != nil { - return err - } - - err = c.Manager.RemoveRepo(name) - if err != nil { - return err - } - - msg, _ := json.Marshal(name + " has been removed from the list of chart repositories.") - util.LogHandlerExitWithJSON(handler, w, msg, http.StatusOK) - return nil -} - -func getChartRepoHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get repository" - util.LogHandlerEntry(handler, r) - repoURL, err := pos(w, r, 2) - if err != nil { - return err - } - - cr, err := c.Manager.GetRepo(repoURL) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, cr, http.StatusOK) - return nil -} - -func listRepoChartsHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: list repository charts" - util.LogHandlerEntry(handler, r) - repoURL, err := pos(w, r, 2) - if err != nil { - return err - } - - values, err := url.ParseQuery(r.URL.RawQuery) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - var regex *regexp.Regexp - regexString := values.Get("regex") - if regexString != "" { - regex, err = regexp.Compile(regexString) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - } - - repoCharts, err := c.Manager.ListRepoCharts(repoURL, regex) - if err != nil { - return err - } - - util.LogHandlerExitWithJSON(handler, w, repoCharts, http.StatusOK) - return nil -} - -func getRepoChartHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get repository charts" - util.LogHandlerEntry(handler, r) - repoURL, err := pos(w, r, 2) - if err != nil { - return err - } - - chartName, err := pos(w, r, 4) - if err != nil { - return err - } - - repoChart, err := c.Manager.GetChartForRepo(repoURL, chartName) - if err != nil { - return err - } - - util.LogHandlerExitWithJSON(handler, w, repoChart, http.StatusOK) - return nil -} diff --git a/cmd/manager/chartrepos_test.go b/cmd/manager/chartrepos_test.go deleted file mode 100644 index 4662bac6d..000000000 --- a/cmd/manager/chartrepos_test.go +++ /dev/null @@ -1,174 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "github.com/kubernetes/helm/pkg/repo" - - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "testing" -) - -var ( - TestRepoBucket = "kubernetes-charts-testing" - TestRepoURL = "gs://" + TestRepoBucket - TestChartName = "frobnitz-0.0.1.tgz" - TestRepoType = string(repo.GCSRepoType) - TestRepoFormat = string(repo.GCSRepoFormat) - TestRepoCredentialName = "default" -) - -func TestListChartRepos(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /repositories", listChartReposHandlerFunc) - defer s.Close() - - URL := getTestURL(t, s.URL, "", "") - res, err := http.Get(URL) - if err != nil { - t.Fatalf("Failed GET: %s", err) - } - - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status %d, got %d", http.StatusOK, res.StatusCode) - } -} - -func TestGetChartRepo(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /repositories/*", getChartRepoHandlerFunc) - defer s.Close() - - URL := getTestURL(t, s.URL, TestRepoBucket, "") - res, err := http.Get(URL) - if err != nil { - t.Fatalf("Failed GET: %s", err) - } - - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status %d, got %d", http.StatusOK, res.StatusCode) - } -} - -func TestListRepoCharts(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /repositories/*/charts", listRepoChartsHandlerFunc) - defer s.Close() - - URL := getTestURL(t, s.URL, TestRepoBucket, "charts") - res, err := http.Get(URL) - if err != nil { - t.Fatalf("Failed GET: %s", err) - } - - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status %d, got %d", http.StatusOK, res.StatusCode) - } -} - -func TestGetRepoChart(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /repositories/*/charts/*", getRepoChartHandlerFunc) - defer s.Close() - - chartURL := fmt.Sprintf("charts/%s", TestChartName) - URL := getTestURL(t, s.URL, TestRepoBucket, chartURL) - res, err := http.Get(URL) - if err != nil { - t.Fatalf("Failed GET: %s", err) - } - - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status %d, got %d", http.StatusOK, res.StatusCode) - } -} - -func TestAddChartRepo(t *testing.T) { - c := stubContext() - s := httpHarness(c, "POST /repositories", addChartRepoHandlerFunc) - defer s.Close() - - URL := getTestURL(t, s.URL, "", "") - body := getTestRepo(t, URL) - res, err := http.Post(URL, "application/json", body) - if err != nil { - t.Fatalf("Failed POST: %s", err) - } - - if res.StatusCode != http.StatusCreated { - t.Errorf("Expected status %d, got %d", http.StatusOK, res.StatusCode) - } -} - -func TestRemoveChartRepo(t *testing.T) { - c := stubContext() - s := httpHarness(c, "DELETE /repositories/*", removeChartRepoHandlerFunc) - defer s.Close() - - URL := getTestURL(t, s.URL, TestRepoBucket, "") - req, err := http.NewRequest("DELETE", URL, nil) - if err != nil { - t.Fatalf("Cannot create DELETE request: %s", err) - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("Failed DELETE: %s", err) - } - - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status %d, got %d", http.StatusOK, res.StatusCode) - } -} - -func getTestRepo(t *testing.T, URL string) io.Reader { - tr, err := repo.NewRepo(URL, TestRepoCredentialName, TestRepoBucket, TestRepoFormat, TestRepoType) - if err != nil { - t.Fatalf("Cannot create test repository: %s", err) - } - - trb, err := json.Marshal(&tr) - if err != nil { - t.Fatalf("Cannot marshal test repository: %s", err) - } - - return bytes.NewReader(trb) -} - -func getTestURL(t *testing.T, baseURL, repoURL, chartURL string) string { - URL := fmt.Sprintf("%s/repositories", baseURL) - if repoURL != "" { - URL = fmt.Sprintf("%s/%s", URL, repoURL) - } - - if chartURL != "" { - URL = fmt.Sprintf("%s/%s", URL, chartURL) - } - - u, err := url.Parse(URL) - if err != nil { - t.Fatalf("cannot parse test URL %s: %s", URL, err) - } - - return u.String() -} diff --git a/cmd/manager/deployments.go b/cmd/manager/deployments.go deleted file mode 100644 index 7948d17f3..000000000 --- a/cmd/manager/deployments.go +++ /dev/null @@ -1,425 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "fmt" - "log" - "net/http" - "strings" - - "github.com/kubernetes/helm/cmd/manager/manager" - "github.com/kubernetes/helm/cmd/manager/repository" - "github.com/kubernetes/helm/cmd/manager/repository/persistent" - "github.com/kubernetes/helm/cmd/manager/repository/transient" - "github.com/kubernetes/helm/cmd/manager/router" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/httputil" - "github.com/kubernetes/helm/pkg/repo" - "github.com/kubernetes/helm/pkg/util" -) - -func registerDeploymentRoutes(c *router.Context, h *router.Handler) { - h.Add("GET /healthz", healthz) - h.Add("GET /deployments", listDeploymentsHandlerFunc) - h.Add("GET /deployments/*", getDeploymentHandlerFunc) - h.Add("POST /deployments", createDeploymentHandlerFunc) - h.Add("DELETE /deployments/*", deleteDeploymentHandlerFunc) - h.Add("PUT /deployments/*", putDeploymentHandlerFunc) - h.Add("GET /deployments/*/manifests", listManifestsHandlerFunc) - h.Add("GET /deployments/*/manifests/*", getManifestHandlerFunc) - h.Add("POST /expand", expandHandlerFunc) - h.Add("GET /charts", listChartsHandlerFunc) - h.Add("GET /charts/*/instances", listChartInstancesHandlerFunc) - h.Add("GET /charts/*/repository", getRepoForChartHandlerFunc) - h.Add("GET /charts/*/metadata", getMetadataForChartHandlerFunc) - h.Add("GET /charts/*", getChartHandlerFunc) - h.Add("POST /credentials/*", createCredentialHandlerFunc) - h.Add("GET /credentials/*", getCredentialHandlerFunc) -} - -func healthz(w http.ResponseWriter, r *http.Request, c *router.Context) error { - log.Println("manager: healthz checkpoint") - // TODO: This should check the availability of the repository, and fail if it - // cannot connect. - fmt.Fprintln(w, "OK") - return nil -} - -func setupDependencies(c *router.Context) error { - var credentialProvider repo.ICredentialProvider - if c.Config.CredentialFile != "" { - if c.Config.CredentialSecrets { - return errors.New("Both credentialFile and credentialSecrets are set") - } - var err error - credentialProvider, err = repo.NewFilebasedCredentialProvider(c.Config.CredentialFile) - if err != nil { - return fmt.Errorf("cannot create credential provider %s: %s", c.Config.CredentialFile, err) - } - } else if *credentialSecrets { - credentialProvider = repo.NewSecretsCredentialProvider() - } else { - credentialProvider = repo.NewInmemCredentialProvider() - } - c.CredentialProvider = credentialProvider - c.Manager = newManager(c) - - return nil -} - -func newManager(c *router.Context) manager.Manager { - cfg := c.Config - service := repo.NewInmemRepoService() - cp := c.CredentialProvider - rp := repo.NewRepoProvider(service, repo.NewGCSRepoProvider(cp), cp) - expander := manager.NewExpander(cfg.ExpanderPort, cfg.ExpanderURL, rp) - deployer := manager.NewDeployer(util.GetServiceURLOrDie(cfg.DeployerName, cfg.DeployerPort, cfg.DeployerURL)) - address := strings.TrimPrefix(util.GetServiceURLOrDie(cfg.MongoName, cfg.MongoPort, cfg.MongoAddress), "http://") - repository := createRepository(address) - return manager.NewManager(expander, deployer, repository, rp, service, c.CredentialProvider) -} - -func createRepository(address string) repository.Repository { - r, err := persistent.NewRepository(address) - if err != nil { - r = transient.NewRepository() - } - - return r -} - -func listDeploymentsHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: list deployments" - util.LogHandlerEntry(handler, r) - l, err := c.Manager.ListDeployments() - if err != nil { - util.LogAndReturnError(handler, http.StatusInternalServerError, err, w) - return nil - } - var names []string - for _, d := range l { - names = append(names, d.Name) - } - - util.LogHandlerExitWithJSON(handler, w, names, http.StatusOK) - return nil -} - -func getDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get deployment" - util.LogHandlerEntry(handler, r) - name, err := pos(w, r, 2) - if err != nil { - return nil - } - - d, err := c.Manager.GetDeployment(name) - if err != nil { - util.LogAndReturnError(handler, http.StatusBadRequest, err, w) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, d, http.StatusOK) - return nil -} - -func createDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: create deployment" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - depReq := getDeploymentRequest(w, r, handler) - if depReq != nil { - d, err := c.Manager.CreateDeployment(depReq) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated) - } - - return nil -} - -func deleteDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: delete deployment" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - name, err := pos(w, r, 2) - if err != nil { - return err - } - - d, err := c.Manager.DeleteDeployment(name, true) - if err != nil { - return err - } - - util.LogHandlerExitWithJSON(handler, w, d, http.StatusOK) - return nil -} - -func putDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: update deployment" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - name, err := pos(w, r, 2) - if err != nil { - return err - } - - depReq := getDeploymentRequest(w, r, handler) - if depReq != nil { - d, err := c.Manager.PutDeployment(name, depReq) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated) - } - - return nil -} - -// pos gets a path item by position. -// -// For example. the path "/foo/bar" has three positions: "" at 0, "foo" at -// 1, and "bar" at 2. -// -// For verb/path combos, position 0 will be the verb: "GET /foo/bar" will have -// "GET " at position 0. -func pos(w http.ResponseWriter, r *http.Request, i int) (string, error) { - parts := strings.Split(r.URL.Path, "/") - if len(parts) < i-1 { - return "", fmt.Errorf("No index for %d", i) - } - return parts[i], nil -} - -func getDeploymentRequest(w http.ResponseWriter, r *http.Request, handler string) *common.DeploymentRequest { - util.LogHandlerEntry(handler, r) - depReq := &common.DeploymentRequest{} - if err := httputil.Decode(w, r, depReq); err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - return depReq -} - -func listManifestsHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: list manifests" - util.LogHandlerEntry(handler, r) - deploymentName, err := pos(w, r, 2) - if err != nil { - return err - } - - m, err := c.Manager.ListManifests(deploymentName) - if err != nil { - return err - } - - var manifestNames []string - for _, manifest := range m { - manifestNames = append(manifestNames, manifest.Name) - } - - util.LogHandlerExitWithJSON(handler, w, manifestNames, http.StatusOK) - return nil -} - -func getManifestHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get manifest" - util.LogHandlerEntry(handler, r) - deploymentName, err := pos(w, r, 2) - if err != nil { - return err - } - - manifestName, err := pos(w, r, 4) - if err != nil { - return err - } - - m, err := c.Manager.GetManifest(deploymentName, manifestName) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, m, http.StatusOK) - return nil -} - -func expandHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: expand config" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - depReq := getDeploymentRequest(w, r, handler) - if depReq != nil { - c, err := c.Manager.Expand(depReq) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, c, http.StatusCreated) - } - - return nil -} - -// Putting Type handlers here for now because deployments.go -// currently owns its own Manager backend and doesn't like to share. -func listChartsHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: list charts" - util.LogHandlerEntry(handler, r) - types, err := c.Manager.ListCharts() - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, types, http.StatusOK) - return nil -} - -func listChartInstancesHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: list chart instances" - util.LogHandlerEntry(handler, r) - chartName, err := pos(w, r, 2) - if err != nil { - return err - } - - instances, err := c.Manager.ListChartInstances(chartName) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, instances, http.StatusOK) - return nil -} - -func getRepoForChartHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get repository for chart" - util.LogHandlerEntry(handler, r) - chartName, err := pos(w, r, 2) - if err != nil { - return err - } - - repository, err := c.Manager.GetRepoForChart(chartName) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, repository, http.StatusOK) - return nil -} - -func getMetadataForChartHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get chart metadata" - util.LogHandlerEntry(handler, r) - chartName, err := pos(w, r, 2) - if err != nil { - return err - } - - metadata, err := c.Manager.GetMetadataForChart(chartName) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, metadata, http.StatusOK) - return nil -} - -func getChartHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get chart" - util.LogHandlerEntry(handler, r) - chartName, err := pos(w, r, 2) - if err != nil { - return err - } - - ch, err := c.Manager.GetChart(chartName) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, ch, http.StatusOK) - return nil -} - -func getCredential(w http.ResponseWriter, r *http.Request, handler string) *repo.Credential { - util.LogHandlerEntry(handler, r) - t := &repo.Credential{} - if err := httputil.Decode(w, r, t); err != nil { - httputil.BadRequest(w, r, err) - return nil - } - return t -} - -func createCredentialHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: create credential" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - credentialName, err := pos(w, r, 2) - if err != nil { - return err - } - - cr := getCredential(w, r, handler) - if cr != nil { - err = c.Manager.CreateCredential(credentialName, cr) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - } - - util.LogHandlerExitWithJSON(handler, w, c, http.StatusOK) - return nil -} - -func getCredentialHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { - handler := "manager: get credential" - util.LogHandlerEntry(handler, r) - credentialName, err := pos(w, r, 2) - if err != nil { - return err - } - - cr, err := c.Manager.GetCredential(credentialName) - if err != nil { - httputil.BadRequest(w, r, err) - return nil - } - - util.LogHandlerExitWithJSON(handler, w, cr, http.StatusOK) - return nil -} diff --git a/cmd/manager/deployments_test.go b/cmd/manager/deployments_test.go deleted file mode 100644 index b3b9023c4..000000000 --- a/cmd/manager/deployments_test.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/kubernetes/helm/pkg/common" -) - -func TestHealthz(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /", healthz) - defer s.Close() - - res, err := http.Get(s.URL) - if err != nil { - t.Fatalf("err on http get: %v", err) - } - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - - if err != nil { - t.Fatalf("Failed to GET healthz: %s", err) - } else if res.StatusCode != 200 { - t.Fatalf("Unexpected status: %d", res.StatusCode) - } - - expectedBody := "OK" - if bytes.Equal(body, []byte(expectedBody)) { - t.Fatalf("Expected response body: %s, Actual response body: %s", - expectedBody, string(body)) - } - - expectedContentType := "text/plain" - contentType := res.Header["Content-Type"][0] - if !strings.Contains(contentType, expectedContentType) { - t.Fatalf("Expected Content-Type to include %s", expectedContentType) - } -} - -func TestCreateDeployments(t *testing.T) { - c := stubContext() - depReq := &common.DeploymentRequest{Name: "foo"} - s := httpHarness(c, "POST /deployments", createDeploymentHandlerFunc) - defer s.Close() - - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(depReq); err != nil { - t.Fatal(err) - } - - res, err := http.Post(s.URL+"/deployments", "application/json", &b) - if err != nil { - t.Errorf("Failed POST: %s", err) - } else if res.StatusCode != http.StatusCreated { - t.Errorf("Expected status %d, got %d", http.StatusCreated, res.StatusCode) - } -} - -func TestListDeployments(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /deployments", listDeploymentsHandlerFunc) - defer s.Close() - - man := c.Manager.(*mockManager) - man.deployments = []*common.Deployment{ - {Name: "one", State: &common.DeploymentState{Status: common.CreatedStatus}}, - {Name: "two", State: &common.DeploymentState{Status: common.DeployedStatus}}, - } - - res, err := http.Get(s.URL + "/deployments") - if err != nil { - t.Errorf("Failed GET: %s", err) - } else if res.StatusCode != http.StatusOK { - t.Errorf("Unexpected status code: %d", res.StatusCode) - } - - var out []string - if err := json.NewDecoder(res.Body).Decode(&out); err != nil { - t.Errorf("Failed to parse results: %s", err) - return - } - if len(out) != 2 { - t.Errorf("Expected 2 names, got %d", len(out)) - } -} - -func TestGetDeployments(t *testing.T) { - c := stubContext() - s := httpHarness(c, "GET /deployments/*", getDeploymentHandlerFunc) - defer s.Close() - - man := c.Manager.(*mockManager) - man.deployments = []*common.Deployment{ - {Name: "portunes", State: &common.DeploymentState{Status: common.CreatedStatus}}, - } - - res, err := http.Get(s.URL + "/deployments/portunes") - if err != nil { - t.Errorf("Failed GET: %s", err) - } else if res.StatusCode != http.StatusOK { - t.Errorf("Unexpected status code: %d", res.StatusCode) - } - - var out common.Deployment - if err := json.NewDecoder(res.Body).Decode(&out); err != nil { - t.Errorf("Failed to parse results: %s", err) - return - } - - if out.Name != "portunes" { - t.Errorf("Unexpected name %q", out.Name) - } - - if out.State.Status != common.CreatedStatus { - t.Errorf("Unexpected status %v", out.State.Status) - } -} - -func TestDeleteDeployments(t *testing.T) { - c := stubContext() - s := httpHarness(c, "DELETE /deployments/*", deleteDeploymentHandlerFunc) - defer s.Close() - - man := c.Manager.(*mockManager) - man.deployments = []*common.Deployment{ - {Name: "portunes", State: &common.DeploymentState{Status: common.CreatedStatus}}, - } - - req, err := http.NewRequest("DELETE", s.URL+"/deployments/portunes", nil) - if err != nil { - t.Fatal("Failed to create delete request") - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("Failed to execute delete request: %s", err) - } - - if res.StatusCode != 200 { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } - - var out common.Deployment - if err := json.NewDecoder(res.Body).Decode(&out); err != nil { - t.Errorf("Failed to parse results: %s", err) - return - } - - if out.Name != "portunes" { - t.Errorf("Unexpected name %q", out.Name) - } -} - -func TestPutDeployment(t *testing.T) { - c := stubContext() - s := httpHarness(c, "PUT /deployments/*", putDeploymentHandlerFunc) - defer s.Close() - - man := c.Manager.(*mockManager) - man.deployments = []*common.Deployment{ - {Name: "demeter", State: &common.DeploymentState{Status: common.CreatedStatus}}, - } - - depreq := &common.DeploymentRequest{Name: "demeter"} - depreq.Configuration = common.Configuration{Resources: []*common.Resource{}} - out, err := json.Marshal(depreq) - if err != nil { - t.Fatalf("Failed to marshal DeploymentRequest: %s", err) - } - - req, err := http.NewRequest("PUT", s.URL+"/deployments/demeter", bytes.NewBuffer(out)) - if err != nil { - t.Fatal("Failed to create PUT request") - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("Failed to execute PUT request: %s", err) - } - - if res.StatusCode != 201 { - t.Errorf("Expected status code 201, got %d", res.StatusCode) - } - - d := &common.Deployment{} - if err := json.NewDecoder(res.Body).Decode(&d); err != nil { - t.Errorf("Failed to parse results: %s", err) - return - } - - if d.Name != "demeter" { - t.Errorf("Unexpected name %q", d.Name) - } -} diff --git a/cmd/manager/main.go b/cmd/manager/main.go deleted file mode 100644 index c6747d05d..000000000 --- a/cmd/manager/main.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "flag" - "fmt" - "log" - "net/http" - "os" - - "github.com/kubernetes/helm/cmd/manager/router" - "github.com/kubernetes/helm/pkg/httputil" - "github.com/kubernetes/helm/pkg/version" -) - -var ( - port = flag.Int("port", 8080, "The port to listen on") - maxLength = flag.Int64("maxLength", 1024, "The maximum length (KB) of a template.") - expanderPort = flag.String("expanderPort", "8081", "The IP port of the default expander service.") - expanderURL = flag.String("expanderURL", "", "The URL for the default expander service.") - deployerName = flag.String("deployer", "resourcifier-service", "The DNS name of the deployer service.") - deployerPort = flag.String("deployerPort", "8082", "The IP port of the deployer service.") - deployerURL = flag.String("deployerURL", "", "The URL for the deployer service.") - credentialFile = flag.String("credentialFile", "", "Local file to use for credentials.") - credentialSecrets = flag.Bool("credentialSecrets", true, "Use secrets for credentials.") - mongoName = flag.String("mongoName", "mongodb", "The DNS name of the mongodb service.") - mongoPort = flag.String("mongoPort", "27017", "The port of the mongodb service.") - mongoAddress = flag.String("mongoAddress", "mongodb:27017", "The address of the mongodb service.") -) - -func main() { - // Set up dependencies - c := &router.Context{ - Config: parseFlags(), - } - - if err := setupDependencies(c); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - httputil.DefaultEncoder.MaxReadLen = c.Config.MaxTemplateLength - - // Set up routes - handler := router.NewHandler(c) - registerDeploymentRoutes(c, handler) - registerChartRepoRoutes(c, handler) - - // Now create a server. - log.Printf("Starting Manager %s on %s", version.Version, c.Config.Address) - if err := http.ListenAndServe(c.Config.Address, handler); err != nil { - log.Printf("Server exited with error %s", err) - os.Exit(1) - } -} - -func parseFlags() *router.Config { - flag.Parse() - return &router.Config{ - Address: fmt.Sprintf(":%d", *port), - MaxTemplateLength: *maxLength, - ExpanderPort: *expanderPort, - ExpanderURL: *expanderURL, - DeployerName: *deployerName, - DeployerPort: *deployerPort, - DeployerURL: *deployerURL, - CredentialFile: *credentialFile, - CredentialSecrets: *credentialSecrets, - MongoName: *mongoName, - MongoPort: *mongoPort, - MongoAddress: *mongoAddress, - } -} diff --git a/cmd/manager/manager/deployer.go b/cmd/manager/manager/deployer.go deleted file mode 100644 index 52179886b..000000000 --- a/cmd/manager/manager/deployer.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package manager - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "strings" - "time" - - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/common" -) - -// Deployer abstracts interactions with the expander and deployer services. -type Deployer interface { - GetConfiguration(cached *common.Configuration) (*common.Configuration, error) - CreateConfiguration(configuration *common.Configuration) (*common.Configuration, error) - DeleteConfiguration(configuration *common.Configuration) (*common.Configuration, error) - PutConfiguration(configuration *common.Configuration) (*common.Configuration, error) -} - -// NewDeployer returns a new initialized Deployer. -// TODO(vaikas): Add a flag for setting the timeout. -func NewDeployer(url string) Deployer { - return &deployer{url, 15} -} - -type deployer struct { - deployerURL string - timeout int -} - -func (d *deployer) getBaseURL() string { - return fmt.Sprintf("%s/configurations", d.deployerURL) -} - -type formatter func(err error) error - -// GetConfiguration reads and returns the actual configuration -// of the resources described by a cached configuration. -func (d *deployer) GetConfiguration(cached *common.Configuration) (*common.Configuration, error) { - errors := &Error{} - actual := &common.Configuration{} - for _, resource := range cached.Resources { - rtype := url.QueryEscape(resource.Type) - rname := url.QueryEscape(resource.Name) - url := fmt.Sprintf("%s/%s/%s", d.getBaseURL(), rtype, rname) - body, err := d.callService("GET", url, nil, func(e error) error { - return fmt.Errorf("cannot get configuration for resource (%s)", e) - }) - if err != nil { - log.Println(errors.appendError(err)) - continue - } - - if len(body) != 0 { - result := &common.Resource{Name: resource.Name, Type: resource.Type} - if err := yaml.Unmarshal(body, &result.Properties); err != nil { - return nil, fmt.Errorf("cannot get configuration for resource (%v)", err) - } - - actual.Resources = append(actual.Resources, result) - } - } - - if len(errors.errors) > 0 { - return nil, errors - } - - return actual, nil -} - -// CreateConfiguration deploys the set of resources described by a configuration and returns -// the Configuration with status for each resource filled in. -func (d *deployer) CreateConfiguration(configuration *common.Configuration) (*common.Configuration, error) { - return d.callServiceWithConfiguration("POST", "create", configuration) -} - -// DeleteConfiguration deletes the set of resources described by a configuration. -func (d *deployer) DeleteConfiguration(configuration *common.Configuration) (*common.Configuration, error) { - return d.callServiceWithConfiguration("DELETE", "delete", configuration) -} - -// PutConfiguration replaces the set of resources described by a configuration and returns -// the Configuration with status for each resource filled in. -func (d *deployer) PutConfiguration(configuration *common.Configuration) (*common.Configuration, error) { - return d.callServiceWithConfiguration("PUT", "replace", configuration) -} - -func (d *deployer) callServiceWithConfiguration(method, operation string, configuration *common.Configuration) (*common.Configuration, error) { - callback := func(e error) error { - return fmt.Errorf("cannot %s configuration: %s", operation, e) - } - - y, err := yaml.Marshal(configuration) - if err != nil { - return nil, callback(err) - } - - reader := ioutil.NopCloser(bytes.NewReader(y)) - resp, err := d.callService(method, d.getBaseURL(), reader, callback) - - if err != nil { - return nil, err - } - - result := &common.Configuration{} - if len(resp) != 0 { - if err := yaml.Unmarshal(resp, &result); err != nil { - return nil, fmt.Errorf("cannot unmarshal response: (%v)", err) - } - } - return result, nil -} - -func (d *deployer) callService(method, url string, reader io.Reader, callback formatter) ([]byte, error) { - request, err := http.NewRequest(method, url, reader) - if err != nil { - return nil, callback(err) - } - - if method != "GET" { - request.Header.Add("Content-Type", "application/json") - } - - timeout := time.Duration(time.Duration(d.timeout) * time.Second) - client := http.Client{Timeout: timeout} - response, err := client.Do(request) - if err != nil { - return nil, callback(err) - } - - defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, callback(err) - } - - if response.StatusCode < http.StatusOK || - response.StatusCode >= http.StatusMultipleChoices { - err := fmt.Errorf("resourcifier response:\n%s", body) - return nil, callback(err) - } - - return body, nil -} - -// Error is an error type that captures errors from the multiple calls to kubectl -// made for a single configuration. -type Error struct { - errors []error -} - -// Error returns the string value of an Error. -func (e *Error) Error() string { - errs := []string{} - for _, err := range e.errors { - errs = append(errs, err.Error()) - } - - return strings.Join(errs, "\n") -} - -func (e *Error) appendError(err error) error { - e.errors = append(e.errors, err) - return err -} diff --git a/cmd/manager/manager/deployer_test.go b/cmd/manager/manager/deployer_test.go deleted file mode 100644 index bab7fb463..000000000 --- a/cmd/manager/manager/deployer_test.go +++ /dev/null @@ -1,304 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package manager - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "path" - "reflect" - "strings" - "testing" - - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/util" - - "github.com/ghodss/yaml" -) - -var validConfigurationTestCaseData = []byte(` -resources: - - name: test-controller-v1 - type: ReplicationController - properties: - kind: ReplicationController - apiVersion: v1 - metadata: - name: test-controller-v1 - namespace: dm - labels: - k8s-app: test - version: v1 - spec: - replicas: 1 - selector: - k8s-app: test - version: v1 - template: - metadata: - labels: - k8s-app: test - version: v1 - spec: - containers: - - name: test - image: deployer/test:latest - ports: - - name: test - containerPort: 8080 - protocol: TCP - - name: test - type: Service - properties: - apiVersion: v1 - kind: Service - metadata: - name: test - namespace: dm - labels: - k8s-app: test - version: v1 - spec: - type: LoadBalancer - selector: - k8s-app: test - version: v1 - ports: - - name: test - port: 8080 - targetPort: test - protocol: TCP -`) - -type DeployerTestCases struct { - TestCases []DeployerTestCase -} - -type DeployerTestCase struct { - Description string - Error string - Handler func(w http.ResponseWriter, r *http.Request) -} - -func TestGetConfiguration(t *testing.T) { - valid := getValidConfiguration(t) - tests := []DeployerTestCase{ - { - "expect success for GetConfiguration", - "", - func(w http.ResponseWriter, r *http.Request) { - // Get name from path, find in valid, and return its properties. - rtype := path.Base(path.Dir(r.URL.Path)) - rname := path.Base(r.URL.Path) - for _, resource := range valid.Resources { - if resource.Type == rtype && resource.Name == rname { - util.LogHandlerExitWithYAML("resourcifier: get configuration", w, resource.Properties, http.StatusOK) - return - } - } - - status := fmt.Sprintf("resource %s of type %s not found", rname, rtype) - http.Error(w, status, http.StatusInternalServerError) - }, - }, - { - "expect error for GetConfiguration", - "cannot get configuration", - deployerErrorHandler, - }, - } - - for _, dtc := range tests { - ts := httptest.NewServer(http.HandlerFunc(dtc.Handler)) - defer ts.Close() - - deployer := NewDeployer(ts.URL) - result, err := deployer.GetConfiguration(valid) - if err != nil { - message := err.Error() - if !strings.Contains(message, dtc.Error) { - t.Errorf("error in test case:%s:%s\n", dtc.Description, message) - } - } else { - if dtc.Error != "" { - t.Errorf("expected error:%s\ndid not occur in test case:%s\n", - dtc.Error, dtc.Description) - } - - if !reflect.DeepEqual(valid, result) { - t.Errorf("error in test case:%s:\nwant:%s\nhave:%s\n", - dtc.Description, util.ToYAMLOrError(valid), util.ToYAMLOrError(result)) - } - } - } -} - -func TestCreateConfiguration(t *testing.T) { - valid := getValidConfiguration(t) - tests := []DeployerTestCase{ - { - "expect success for CreateConfiguration", - "", - deployerSuccessHandler, - }, - { - "expect error for CreateConfiguration", - "cannot create configuration", - deployerErrorHandler, - }, - } - - for _, dtc := range tests { - ts := httptest.NewServer(http.HandlerFunc(dtc.Handler)) - defer ts.Close() - - deployer := NewDeployer(ts.URL) - _, err := deployer.CreateConfiguration(valid) - if err != nil { - message := err.Error() - if !strings.Contains(message, dtc.Error) { - t.Errorf("error in test case:%s:%s\n", dtc.Description, message) - } - } else { - if dtc.Error != "" { - t.Errorf("expected error:%s\ndid not occur in test case:%s\n", - dtc.Error, dtc.Description) - } - } - } -} - -func TestDeleteConfiguration(t *testing.T) { - valid := getValidConfiguration(t) - tests := []DeployerTestCase{ - { - "expect success for DeleteConfiguration", - "", - deployerSuccessHandler, - }, - { - "expect error for DeleteConfiguration", - "cannot delete configuration", - deployerErrorHandler, - }, - } - - for _, dtc := range tests { - ts := httptest.NewServer(http.HandlerFunc(dtc.Handler)) - defer ts.Close() - - deployer := NewDeployer(ts.URL) - _, err := deployer.DeleteConfiguration(valid) - if err != nil { - message := err.Error() - if !strings.Contains(message, dtc.Error) { - t.Errorf("error in test case:%s:%s\n", dtc.Description, message) - } - } else { - if dtc.Error != "" { - t.Errorf("expected error:%s\ndid not occur in test case:%s\n", - dtc.Error, dtc.Description) - } - } - } -} - -func TestPutConfiguration(t *testing.T) { - valid := getValidConfiguration(t) - tests := []DeployerTestCase{ - { - "expect success for PutConfiguration", - "", - deployerSuccessHandler, - }, - { - "expect error for PutConfiguration", - "cannot replace configuration", - deployerErrorHandler, - }, - } - - for _, dtc := range tests { - ts := httptest.NewServer(http.HandlerFunc(dtc.Handler)) - defer ts.Close() - - deployer := NewDeployer(ts.URL) - _, err := deployer.PutConfiguration(valid) - if err != nil { - message := err.Error() - if !strings.Contains(message, dtc.Error) { - t.Errorf("error in test case:%s:%s\n", dtc.Description, message) - } - } else { - if dtc.Error != "" { - t.Errorf("expected error:%s\ndid not occur in test case:%s\n", - dtc.Error, dtc.Description) - } - } - } -} - -func getValidConfiguration(t *testing.T) *common.Configuration { - valid := &common.Configuration{} - err := yaml.Unmarshal(validConfigurationTestCaseData, valid) - if err != nil { - t.Errorf("cannot unmarshal test case data:%s\n", err) - } - - return valid -} - -func deployerErrorHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - http.Error(w, "something failed", http.StatusInternalServerError) -} - -func deployerSuccessHandler(w http.ResponseWriter, r *http.Request) { - valid := &common.Configuration{} - err := yaml.Unmarshal(validConfigurationTestCaseData, valid) - if err != nil { - status := fmt.Sprintf("cannot unmarshal test case data:%s", err) - http.Error(w, status, http.StatusInternalServerError) - return - } - - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - if err != nil { - status := fmt.Sprintf("cannot read request body:%s", err) - http.Error(w, status, http.StatusInternalServerError) - return - } - - result := &common.Configuration{} - if err := yaml.Unmarshal(body, result); err != nil { - status := fmt.Sprintf("cannot unmarshal request body:%s", err) - http.Error(w, status, http.StatusInternalServerError) - return - } - - if !reflect.DeepEqual(valid, result) { - status := fmt.Sprintf("error in http handler:\nwant:%s\nhave:%s\n", - util.ToYAMLOrError(valid), util.ToYAMLOrError(result)) - http.Error(w, status, http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} diff --git a/cmd/manager/manager/expander.go b/cmd/manager/manager/expander.go deleted file mode 100644 index 4f4c081b4..000000000 --- a/cmd/manager/manager/expander.go +++ /dev/null @@ -1,225 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package manager - -import ( - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/expansion" - "github.com/kubernetes/helm/pkg/repo" - "github.com/kubernetes/helm/pkg/util" - - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" -) - -// ExpandedConfiguration is the structure returned by the expansion service. -type ExpandedConfiguration struct { - Config *common.Configuration `json:"config"` - Layout *common.Layout `json:"layout"` -} - -// Expander abstracts interactions with the expander and deployer services. -type Expander interface { - ExpandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) -} - -// NewExpander returns a new initialized Expander. -func NewExpander(port, URL string, rp repo.IRepoProvider) Expander { - if rp == nil { - rp = repo.NewRepoProvider(nil, nil, nil) - } - - return &expander{expanderPort: port, expanderURL: URL, repoProvider: rp} -} - -type expander struct { - repoProvider repo.IRepoProvider - expanderPort string - expanderURL string -} - -// ExpandConfiguration expands the supplied configuration and returns -// an expanded configuration. -func (e *expander) ExpandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) { - expConf, err := e.expandConfiguration(conf) - if err != nil { - return nil, err - } - - return expConf, nil -} - -func (e *expander) expandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) { - resources := []*common.Resource{} - layouts := []*common.LayoutResource{} - - // Iterate over all of the resources in the unexpanded configuration - for _, resource := range conf.Resources { - additions := []*common.Resource{resource} - layout := &common.LayoutResource{ - Resource: common.Resource{ - Name: resource.Name, Type: resource.Type, - }, - } - - // If the type is a chart reference - if repo.IsChartReference(resource.Type) { - // Fetch, decompress and unpack - cbr, _, err := e.repoProvider.GetChartByReference(resource.Type) - if err != nil { - return nil, err - } - - defer cbr.Close() - - // Load the charts contents into strings that we can pass to exapnsion - content, err := cbr.LoadContent() - if err != nil { - return nil, err - } - - expander := cbr.Chartfile().Expander - if expander != nil && expander.Name != "" { - // Build a request to the expansion service and call it to do the expansion - svcReq := &expansion.ServiceRequest{ - ChartInvocation: resource, - Chart: content, - } - - svcResp, err := e.callService(expander.Name, svcReq) - if err != nil { - return nil, err - } - - // Call ourselves recursively with the list of resources returned by expansion - expConf, err := e.expandConfiguration(svcResp) - if err != nil { - return nil, err - } - - // Append the reources returned by the recursion to the flat list of resources - additions = expConf.Config.Resources - - // This was not a primitive resource, so add its properties to the layout - // Then add the all of the layout resources returned by the recursion to the layout - layout.Resources = expConf.Layout.Resources - layout.Properties = resource.Properties - } else { - // Raise an error if a non template chart supplies properties - if resource.Properties != nil { - return nil, fmt.Errorf("properties provided for non template chart %s", resource.Type) - } - - additions = []*common.Resource{} - for _, member := range content.Members { - segments := strings.Split(member.Path, "/") - if len(segments) > 1 && segments[0] == "templates" { - if strings.HasSuffix(member.Path, "yaml") || strings.HasSuffix(member.Path, "json") { - resource, err := util.ParseKubernetesObject(member.Content) - if err != nil { - return nil, err - } - - resources = append(resources, resource) - } - } - } - } - } - - resources = append(resources, additions...) - layouts = append(layouts, layout) - } - - // All done with this level, so return the expanded configuration - result := &ExpandedConfiguration{ - Config: &common.Configuration{Resources: resources}, - Layout: &common.Layout{Resources: layouts}, - } - - return result, nil -} - -func (e *expander) callService(svcName string, svcReq *expansion.ServiceRequest) (*common.Configuration, error) { - svcURL, err := e.getServiceURL(svcName) - if err != nil { - return nil, err - } - - j, err := json.Marshal(svcReq) - if err != nil { - return nil, err - } - - reader := ioutil.NopCloser(bytes.NewReader(j)) - request, err := http.NewRequest("POST", svcURL, reader) - if err != nil { - return nil, err - } - - request.Header.Set("Content-Type", "application/json") - request.Header.Set("Accept", "*/*") - - response, err := http.DefaultClient.Do(request) - if err != nil { - e := fmt.Errorf("call failed (%s) with payload:\n%s\n", err, string(j)) - return nil, e - } - - defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if err != nil { - e := fmt.Errorf("error reading response: %s", err) - return nil, e - } - - if response.StatusCode != http.StatusOK { - err := fmt.Errorf("expandybird response:\n%s", body) - return nil, err - } - - svcResp := &common.Configuration{} - if err := json.Unmarshal(body, svcResp); err != nil { - e := fmt.Errorf("cannot unmarshal response body (%s):%s", err, body) - return nil, e - } - - return svcResp, nil -} - -func (e *expander) getServiceURL(svcName string) (string, error) { - if !strings.HasPrefix(svcName, "http:") && !strings.HasPrefix(svcName, "https:") { - var err error - svcName, err = util.GetServiceURL(svcName, e.expanderPort, e.expanderURL) - if err != nil { - return "", err - } - } - - u, err := url.Parse(svcName) - if err != nil { - return "", err - } - - u.Path = fmt.Sprintf("%s/expand", u.Path) - return u.String(), nil -} diff --git a/cmd/manager/manager/expander_test.go b/cmd/manager/manager/expander_test.go deleted file mode 100644 index 8ab757581..000000000 --- a/cmd/manager/manager/expander_test.go +++ /dev/null @@ -1,373 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package manager - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "reflect" - "strings" - "testing" - - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/expansion" - "github.com/kubernetes/helm/pkg/repo" - "github.com/kubernetes/helm/pkg/util" -) - -var ( - TestRepoBucket = "kubernetes-charts-testing" - TestRepoURL = "gs://" + TestRepoBucket - TestChartName = "frobnitz" - TestChartVersion = "0.0.1" - TestArchiveName = TestChartName + "-" + TestChartVersion + ".tgz" - TestResourceType = TestRepoURL + "/" + TestArchiveName - TestRepoType = string(repo.GCSRepoType) - TestRepoFormat = string(repo.GCSRepoFormat) - TestRepoCredentialName = "default" - TestRepoName = TestRepoBucket -) - -var validResponseTestCaseData = []byte(` -resources: -- name: test-service - properties: - test-property: test-value - type: Service -- name: test-rc - properties: - test-property: test-value - type: ReplicationController -- name: test3-service - properties: - test-property: test-value - type: Service -- name: test3-rc - properties: - test-property: test-value - type: ReplicationController -- name: test4-service - properties: - test-property: test-value - type: Service -- name: test4-rc - properties: - test-property: test-value - type: ReplicationController -`) - -var validLayoutTestCaseData = []byte(` -resources: -- name: test_invocation - resources: - - name: test-service - type: Service - - name: test-rc - type: ReplicationController - - name: test3-service - type: Service - - name: test3-rc - type: ReplicationController - - name: test4-service - type: Service - - name: test4-rc - type: ReplicationController - type: gs://kubernetes-charts-testing/frobnitz-0.0.1.tgz -`) - -/* -[]byte(` -resources: -- name: test - properties: - test-property: test-value - resources: - - name: test-service - type: Service - - name: test-rc - type: ReplicationController - type: test-type.py -- name: test2 - properties: null - resources: - - name: test3 - properties: - test-property: test-value - resources: - - name: test3-service - type: Service - - name: test3-rc - type: ReplicationController - type: test-type.py - - name: test4 - properties: - test-property: test-value - resources: - - name: test4-service - type: Service - - name: test4-rc - type: ReplicationController - type: test-type.py - type: test2.jinja -`) - -var roundTripContent = ` -resources: -- name: test - type: test.py - properties: - test: test -` - -var roundTripExpanded = ` -resources: -- name: test2 - type: test2.py - properties: - test: test -` - -var roundTripLayout = ` -resources: -- name: test - type: test.py - properties: - test: test - resources: - - name: test2 - type: test2.py - properties: - test: test -` - -var roundTripExpanded2 = ` -resources: -- name: test3 - type: Service - properties: - test: test -` - -var roundTripLayout2 = ` -resources: -- name: test2 - type: test2.py - properties: - test: test - resources: - - name: test3 - type: Service - properties: - test: test -` - -var finalExpanded = ` -config: - resources: - - name: test3 - type: Service - properties: - test: test -layout: - resources: - - name: test - type: test.py - properties: - test: test - resources: - - name: test2 - type: test2.py - properties: - test: test - resources: - - name: test3 - type: Service - properties: - test: test -` - -var roundTripResponse = &ExpandedConfiguration{ - Config: roundTripExpanded, -} - -var roundTripResponse2 = &ExpandedConfiguration{ - Config: roundTripExpanded2, -} - -var roundTripResponses = []*ExpandedConfiguration{ - roundTripResponse, - roundTripResponse2, -} -*/ - -type ExpanderTestCase struct { - Description string - Error string - Handler func(w http.ResponseWriter, r *http.Request) - ValidResponse *ExpandedConfiguration -} - -func TestExpandTemplate(t *testing.T) { - // roundTripResponse := &ExpandedConfiguration{} - // if err := yaml.Unmarshal([]byte(finalExpanded), roundTripResponse); err != nil { - // panic(err) - // } - - tests := []ExpanderTestCase{ - { - "expect success for ExpandConfiguration", - "", - expanderSuccessHandler, - getValidExpandedConfiguration(), - }, - { - "expect error for ExpandConfiguration", - "simulated failure", - expanderErrorHandler, - nil, - }, - } - - for _, etc := range tests { - ts := httptest.NewServer(http.HandlerFunc(etc.Handler)) - defer ts.Close() - - expander := NewExpander("8081", ts.URL, getTestRepoProvider(t)) - resource := &common.Resource{ - Name: "test_invocation", - Type: TestResourceType, - } - - conf := &common.Configuration{ - Resources: []*common.Resource{ - resource, - }, - } - - actualResponse, err := expander.ExpandConfiguration(conf) - if err != nil { - message := err.Error() - if etc.Error == "" { - t.Errorf("unexpected error in test case %s: %s", etc.Description, err) - } - if !strings.Contains(message, etc.Error) { - t.Errorf("error in test case:%s:%s\n", etc.Description, message) - } - } else { - if etc.Error != "" { - t.Errorf("expected error:%s\ndid not occur in test case:%s\n", - etc.Error, etc.Description) - } - - expectedResponse := etc.ValidResponse - if !reflect.DeepEqual(expectedResponse, actualResponse) { - t.Errorf("error in test case:%s:\nwant:%s\nhave:%s\n", - etc.Description, util.ToYAMLOrError(expectedResponse), util.ToYAMLOrError(actualResponse)) - } - } - } -} - -func getValidServiceResponse() *common.Configuration { - conf := &common.Configuration{} - if err := yaml.Unmarshal(validResponseTestCaseData, conf); err != nil { - panic(fmt.Errorf("cannot unmarshal valid response: %s\n", err)) - } - - return conf -} - -func getValidExpandedConfiguration() *ExpandedConfiguration { - conf := getValidServiceResponse() - layout := &common.Layout{} - if err := yaml.Unmarshal(validLayoutTestCaseData, layout); err != nil { - panic(fmt.Errorf("cannot unmarshal valid response: %s\n", err)) - } - - return &ExpandedConfiguration{Config: conf, Layout: layout} - -} - -func expanderErrorHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - http.Error(w, "simulated failure", http.StatusInternalServerError) -} - -/* -func roundTripHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - handler := "expandybird: expand" - util.LogHandlerEntry(handler, r) - if len(roundTripResponses) < 1 { - http.Error(w, "Too many calls to round trip handler", http.StatusInternalServerError) - return - } - - util.LogHandlerExitWithJSON(handler, w, roundTripResponses[0], http.StatusOK) - roundTripResponses = roundTripResponses[1:] -} -*/ - -func expanderSuccessHandler(w http.ResponseWriter, r *http.Request) { - handler := "expandybird: expand" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - if err != nil { - status := fmt.Sprintf("cannot read request body:%s", err) - http.Error(w, status, http.StatusInternalServerError) - return - } - - svcReq := &expansion.ServiceRequest{} - if err := json.Unmarshal(body, svcReq); err != nil { - status := fmt.Sprintf("cannot unmarshal request body:%s\n%s\n", err, body) - http.Error(w, status, http.StatusInternalServerError) - return - } - - /* - if !reflect.DeepEqual(validRequestTestCaseData, *svcReq) { - status := fmt.Sprintf("error in http handler:\nwant:%s\nhave:%s\n", - util.ToJSONOrError(validRequestTestCaseData), util.ToJSONOrError(template)) - http.Error(w, status, http.StatusInternalServerError) - return - } - */ - - svcResp := getValidServiceResponse() - util.LogHandlerExitWithJSON(handler, w, svcResp, http.StatusOK) -} - -func getTestRepoProvider(t *testing.T) repo.IRepoProvider { - rs := repo.NewInmemRepoService() - rp := repo.NewRepoProvider(rs, nil, nil) - tr, err := repo.NewRepo(TestRepoURL, TestRepoCredentialName, TestRepoName, TestRepoFormat, TestRepoType) - if err != nil { - t.Fatalf("cannot create test repository: %s", err) - } - - if err := rs.CreateRepo(tr); err != nil { - t.Fatalf("cannot initialize repository service: %s", err) - } - - return rp -} diff --git a/cmd/manager/manager/manager.go b/cmd/manager/manager/manager.go deleted file mode 100644 index 2b148ac60..000000000 --- a/cmd/manager/manager/manager.go +++ /dev/null @@ -1,447 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package manager - -import ( - "fmt" - "log" - "regexp" - "time" - - "github.com/kubernetes/helm/cmd/manager/repository" - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/repo" -) - -// Manager manages a persistent set of Deployments. -type Manager interface { - // Deployments - ListDeployments() ([]common.Deployment, error) - GetDeployment(name string) (*common.Deployment, error) - CreateDeployment(depReq *common.DeploymentRequest) (*common.Deployment, error) - DeleteDeployment(name string, forget bool) (*common.Deployment, error) - PutDeployment(name string, depReq *common.DeploymentRequest) (*common.Deployment, error) - - // Manifests - ListManifests(deploymentName string) (map[string]*common.Manifest, error) - GetManifest(deploymentName string, manifest string) (*common.Manifest, error) - Expand(t *common.DeploymentRequest) (*common.Manifest, error) - - // Charts - ListCharts() ([]string, error) - ListChartInstances(chartName string) ([]*common.ChartInstance, error) - GetRepoForChart(chartName string) (string, error) - GetMetadataForChart(chartName string) (*chart.Chartfile, error) - GetChart(chartName string) (*chart.Chart, error) - - // Repo Charts - ListRepoCharts(repoName string, regex *regexp.Regexp) ([]string, error) - GetChartForRepo(repoName, chartName string) (*chart.Chart, error) - - // Credentials - CreateCredential(name string, c *repo.Credential) error - GetCredential(name string) (*repo.Credential, error) - - // Chart Repositories - ListRepos() (map[string]string, error) - AddRepo(addition repo.IRepo) error - RemoveRepo(repoName string) error - GetRepo(repoName string) (repo.IRepo, error) -} - -type manager struct { - expander Expander - deployer Deployer - repository repository.Repository - repoProvider repo.IRepoProvider - service repo.IRepoService - //TODO: add chart repo service - credentialProvider repo.ICredentialProvider -} - -// NewManager returns a new initialized Manager. -func NewManager(expander Expander, - deployer Deployer, - repository repository.Repository, - repoProvider repo.IRepoProvider, - service repo.IRepoService, - credentialProvider repo.ICredentialProvider) Manager { - return &manager{expander, deployer, repository, repoProvider, service, credentialProvider} -} - -// ListDeployments returns the list of deployments -func (m *manager) ListDeployments() ([]common.Deployment, error) { - l, err := m.repository.ListDeployments() - if err != nil { - return nil, err - } - return l, nil -} - -// GetDeployment retrieves the configuration stored for a given deployment -// as well as the current configuration from the cluster. -func (m *manager) GetDeployment(name string) (*common.Deployment, error) { - d, err := m.repository.GetDeployment(name) - if err != nil { - return nil, err - } - - return d, nil -} - -// ListManifests retrieves the manifests for a given deployment -// of each of the deployments in the repository and returns the deployments. -func (m *manager) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { - l, err := m.repository.ListManifests(deploymentName) - if err != nil { - return nil, err - } - - return l, nil -} - -// GetManifest retrieves the specified manifest for a given deployment -func (m *manager) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) { - d, err := m.repository.GetManifest(deploymentName, manifestName) - if err != nil { - return nil, err - } - - return d, nil -} - -// CreateDeployment expands the supplied configuration, creates the resulting -// resources in the cluster, creates a new deployment that tracks it, stores the -// deployment in the repository and returns the deployment. -func (m *manager) CreateDeployment(depReq *common.DeploymentRequest) (*common.Deployment, error) { - log.Printf("Creating deployment: %s", depReq.Name) - _, err := m.repository.CreateDeployment(depReq.Name) - if err != nil { - log.Printf("CreateDeployment failed %v", err) - return nil, err - } - - manifest, err := m.Expand(depReq) - if err != nil { - log.Printf("Manifest creation failed: %v", err) - m.repository.SetDeploymentState(depReq.Name, failState(err)) - return nil, err - } - - if err := m.repository.AddManifest(manifest); err != nil { - log.Printf("AddManifest failed %v", err) - m.repository.SetDeploymentState(depReq.Name, failState(err)) - return nil, err - } - - actualConfig, err := m.deployer.CreateConfiguration(manifest.ExpandedConfig) - if err != nil { - // Deployment failed, mark as failed - log.Printf("CreateConfiguration failed: %v", err) - m.repository.SetDeploymentState(depReq.Name, failState(err)) - - // If we failed before being able to create some of the resources, then - // return the failure as such. Otherwise, we're going to add the manifest - // and hence resource specific errors down below. - if actualConfig == nil { - return nil, err - } - } else { - // May be errors in the resources themselves. - errs := getResourceErrors(actualConfig) - if len(errs) > 0 { - e := fmt.Errorf("Found resource errors during deployment: %v", errs) - m.repository.SetDeploymentState(depReq.Name, failState(e)) - return nil, e - } - - m.repository.SetDeploymentState(depReq.Name, &common.DeploymentState{Status: common.DeployedStatus}) - } - - // Update the manifest with the actual state of the reified resources - manifest.ExpandedConfig = actualConfig - if err := m.repository.SetManifest(manifest); err != nil { - log.Printf("SetManifest failed %v", err) - m.repository.SetDeploymentState(depReq.Name, failState(err)) - return nil, err - } - - // Finally update the type instances for this deployment. - m.setChartInstances(depReq.Name, manifest.Name, manifest.Layout) - return m.repository.GetValidDeployment(depReq.Name) -} - -func (m *manager) setChartInstances(deploymentName string, manifestName string, layout *common.Layout) { - m.repository.ClearChartInstancesForDeployment(deploymentName) - - instances := make(map[string][]*common.ChartInstance) - for i, r := range layout.Resources { - addChartInstances(&instances, r, deploymentName, manifestName, fmt.Sprintf("$.resources[%d]", i)) - } - - m.repository.AddChartInstances(instances) -} - -func addChartInstances(instances *map[string][]*common.ChartInstance, r *common.LayoutResource, deploymentName string, manifestName string, jsonPath string) { - // Add this resource. - inst := &common.ChartInstance{ - Name: r.Name, - Type: r.Type, - Deployment: deploymentName, - Manifest: manifestName, - Path: jsonPath, - } - - (*instances)[r.Type] = append((*instances)[r.Type], inst) - - // Add all sub resources if they exist. - for i, sr := range r.Resources { - addChartInstances(instances, sr, deploymentName, manifestName, fmt.Sprintf("%s.resources[%d]", jsonPath, i)) - } -} - -// DeleteDeployment deletes the configuration for the deployment with -// the supplied identifier from the cluster.repository. If forget is true, then -// the deployment is removed from the repository. Otherwise, it is marked -// as deleted and retained. -func (m *manager) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { - log.Printf("Deleting deployment: %s", name) - d, err := m.repository.GetValidDeployment(name) - if err != nil { - return nil, err - } - - // If there's a latest manifest, delete the underlying resources. - latest, err := m.repository.GetLatestManifest(name) - if err != nil { - m.repository.SetDeploymentState(name, failState(err)) - return nil, err - } - - if latest != nil { - log.Printf("Deleting resources from the latest manifest") - // Clear previous state. - for _, r := range latest.ExpandedConfig.Resources { - r.State = nil - } - - if _, err := m.deployer.DeleteConfiguration(latest.ExpandedConfig); err != nil { - log.Printf("Failed to delete resources from the latest manifest: %v", err) - m.repository.SetDeploymentState(name, failState(err)) - return nil, err - } - - // Create an empty manifest since resources have been deleted. - if !forget { - manifest := &common.Manifest{Deployment: name, Name: generateManifestName()} - if err := m.repository.AddManifest(manifest); err != nil { - log.Printf("Failed to add empty manifest") - return nil, err - } - } - } - - d, err = m.repository.DeleteDeployment(name, forget) - if err != nil { - return nil, err - } - - // Finally remove the type instances for this deployment. - m.repository.ClearChartInstancesForDeployment(name) - return d, nil -} - -// PutDeployment replaces the configuration of the deployment with -// the supplied identifier in the cluster, and returns the deployment. -func (m *manager) PutDeployment(name string, depReq *common.DeploymentRequest) (*common.Deployment, error) { - _, err := m.repository.GetValidDeployment(name) - if err != nil { - return nil, err - } - - manifest, err := m.Expand(depReq) - if err != nil { - log.Printf("Manifest creation failed: %v", err) - m.repository.SetDeploymentState(name, failState(err)) - return nil, err - } - - actualConfig, err := m.deployer.PutConfiguration(manifest.ExpandedConfig) - if err != nil { - m.repository.SetDeploymentState(name, failState(err)) - return nil, err - } - - manifest.ExpandedConfig = actualConfig - err = m.repository.AddManifest(manifest) - if err != nil { - m.repository.SetDeploymentState(name, failState(err)) - return nil, err - } - - // Finally update the type instances for this deployment. - m.setChartInstances(depReq.Name, manifest.Name, manifest.Layout) - return m.repository.GetValidDeployment(depReq.Name) -} - -func (m *manager) Expand(depReq *common.DeploymentRequest) (*common.Manifest, error) { - expConf, err := m.expander.ExpandConfiguration(&depReq.Configuration) - if err != nil { - log.Printf("Expansion failed %v", err) - return nil, err - } - - return &common.Manifest{ - Name: generateManifestName(), - Deployment: depReq.Name, - InputConfig: &depReq.Configuration, - ExpandedConfig: expConf.Config, - Layout: expConf.Layout, - }, nil -} - -func (m *manager) ListCharts() ([]string, error) { - return m.repository.ListCharts() -} - -func (m *manager) ListChartInstances(chartName string) ([]*common.ChartInstance, error) { - return m.repository.GetChartInstances(chartName) -} - -// GetRepoForChart returns the repository where the referenced chart resides. -func (m *manager) GetRepoForChart(reference string) (string, error) { - _, r, err := m.repoProvider.GetChartByReference(reference) - if err != nil { - return "", err - } - - return r.GetURL(), nil -} - -// GetMetadataForChart returns the metadata for the referenced chart. -func (m *manager) GetMetadataForChart(reference string) (*chart.Chartfile, error) { - c, _, err := m.repoProvider.GetChartByReference(reference) - if err != nil { - return nil, err - } - - return c.Chartfile(), nil -} - -// GetChart returns the referenced chart. -func (m *manager) GetChart(reference string) (*chart.Chart, error) { - c, _, err := m.repoProvider.GetChartByReference(reference) - if err != nil { - return nil, err - } - - return c, nil -} - -// ListRepos returns the list of available repository URLs -func (m *manager) ListRepos() (map[string]string, error) { - return m.service.ListRepos() -} - -// AddRepo adds a repository to the list -func (m *manager) AddRepo(addition repo.IRepo) error { - return m.service.CreateRepo(addition) -} - -// RemoveRepo removes a repository from the list by URL -func (m *manager) RemoveRepo(repoName string) error { - repoURL, err := m.service.GetRepoURLByName(repoName) - if err != nil { - return err - } - - return m.service.DeleteRepo(repoURL) -} - -// GetRepo returns the repository with the given name -func (m *manager) GetRepo(repoName string) (repo.IRepo, error) { - repoURL, err := m.service.GetRepoURLByName(repoName) - if err != nil { - return nil, err - } - - return m.service.GetRepoByURL(repoURL) -} - -func generateManifestName() string { - return fmt.Sprintf("manifest-%d", time.Now().UTC().UnixNano()) -} - -func failState(e error) *common.DeploymentState { - return &common.DeploymentState{ - Status: common.FailedStatus, - Errors: []string{e.Error()}, - } -} - -func getResourceErrors(c *common.Configuration) []string { - var errs []string - for _, r := range c.Resources { - if r.State.Status == common.Failed { - errs = append(errs, r.State.Errors...) - } - } - - return errs -} - -// ListRepoCharts lists charts in a given repository whose names -// conform to the supplied regular expression, or all charts, if the regular -// expression is nil. -func (m *manager) ListRepoCharts(repoName string, regex *regexp.Regexp) ([]string, error) { - repoURL, err := m.service.GetRepoURLByName(repoName) - if err != nil { - return nil, err - } - - r, err := m.repoProvider.GetRepoByURL(repoURL) - if err != nil { - return nil, err - } - - return r.ListCharts(regex) -} - -// GetChartForRepo returns a chart by name from a given repository. -func (m *manager) GetChartForRepo(repoName, chartName string) (*chart.Chart, error) { - repoURL, err := m.service.GetRepoURLByName(repoName) - if err != nil { - return nil, err - } - - r, err := m.repoProvider.GetRepoByURL(repoURL) - if err != nil { - return nil, err - } - - return r.GetChart(chartName) -} - -// CreateCredential creates a credential that can be used to authenticate to repository -func (m *manager) CreateCredential(name string, c *repo.Credential) error { - return m.credentialProvider.SetCredential(name, c) -} - -func (m *manager) GetCredential(name string) (*repo.Credential, error) { - return m.credentialProvider.GetCredential(name) -} diff --git a/cmd/manager/manager/manager_test.go b/cmd/manager/manager/manager_test.go deleted file mode 100644 index 150431630..000000000 --- a/cmd/manager/manager/manager_test.go +++ /dev/null @@ -1,551 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package manager - -import ( - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/repo" - - "errors" - "reflect" - "strings" - "testing" -) - -var layout = common.Layout{ - Resources: []*common.LayoutResource{{Resource: common.Resource{Name: "test", Type: "test"}}}, -} -var configuration = common.Configuration{ - Resources: []*common.Resource{{Name: "test", Type: "test"}}, -} -var resourcesWithSuccessState = common.Configuration{ - Resources: []*common.Resource{{Name: "test", Type: "test", State: &common.ResourceState{Status: common.Created}}}, -} -var resourcesWithFailureState = common.Configuration{ - Resources: []*common.Resource{{ - Name: "test", - Type: "test", - State: &common.ResourceState{ - Status: common.Failed, - Errors: []string{"test induced error"}, - }, - }}, -} -var deploymentRequest = common.DeploymentRequest{Name: "test", Configuration: configuration} - -var expandedConfig = ExpandedConfiguration{ - Config: &configuration, - Layout: &layout, -} - -var deploymentName = "deployment" - -var manifestName = "manifest-2" -var manifest = common.Manifest{Name: manifestName, ExpandedConfig: &configuration, Layout: &layout} -var manifestMap = map[string]*common.Manifest{manifest.Name: &manifest} - -var deployment = common.Deployment{ - Name: "test", -} - -var deploymentList = []common.Deployment{deployment, {Name: "test2"}} - -var typeInstMap = map[string][]string{"test": {"test"}} - -var errTest = errors.New("test error") - -type expanderStub struct{} - -func (expander *expanderStub) ExpandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) { - if reflect.DeepEqual(conf, &configuration) { - return &expandedConfig, nil - } - - return nil, errTest -} - -type deployerStub struct { - FailCreate bool - Created []*common.Configuration - FailDelete bool - Deleted []*common.Configuration - FailCreateResource bool -} - -func (deployer *deployerStub) reset() { - deployer.FailCreate = false - deployer.Created = make([]*common.Configuration, 0) - deployer.FailDelete = false - deployer.Deleted = make([]*common.Configuration, 0) - deployer.FailCreateResource = false -} - -func newDeployerStub() *deployerStub { - ret := &deployerStub{} - return ret -} - -func (deployer *deployerStub) GetConfiguration(cached *common.Configuration) (*common.Configuration, error) { - return nil, nil -} - -func (deployer *deployerStub) CreateConfiguration(configuration *common.Configuration) (*common.Configuration, error) { - if deployer.FailCreate { - return nil, errTest - } - if deployer.FailCreateResource { - return &resourcesWithFailureState, errTest - } - - deployer.Created = append(deployer.Created, configuration) - return &resourcesWithSuccessState, nil -} - -func (deployer *deployerStub) DeleteConfiguration(configuration *common.Configuration) (*common.Configuration, error) { - if deployer.FailDelete { - return nil, errTest - } - deployer.Deleted = append(deployer.Deleted, configuration) - return nil, nil -} - -func (deployer *deployerStub) PutConfiguration(configuration *common.Configuration) (*common.Configuration, error) { - return nil, nil -} - -type repositoryStub struct { - FailListDeployments bool - Created []string - ManifestAdd map[string]*common.Manifest - ManifestSet map[string]*common.Manifest - Deleted []string - GetValid []string - ChartInstances map[string][]string - ChartInstancesCleared bool - GetChartInstancesCalled bool - ListTypesCalled bool - DeploymentStates []*common.DeploymentState -} - -func (repository *repositoryStub) reset() { - repository.FailListDeployments = false - repository.Created = make([]string, 0) - repository.ManifestAdd = make(map[string]*common.Manifest) - repository.ManifestSet = make(map[string]*common.Manifest) - repository.Deleted = make([]string, 0) - repository.GetValid = make([]string, 0) - repository.ChartInstances = make(map[string][]string) - repository.ChartInstancesCleared = false - repository.GetChartInstancesCalled = false - repository.ListTypesCalled = false - repository.DeploymentStates = []*common.DeploymentState{} -} - -func newRepositoryStub() *repositoryStub { - ret := &repositoryStub{} - return ret -} - -// Deployments. -func (repository *repositoryStub) ListDeployments() ([]common.Deployment, error) { - if repository.FailListDeployments { - return deploymentList, errTest - } - - return deploymentList, nil -} - -func (repository *repositoryStub) GetDeployment(d string) (*common.Deployment, error) { - if d == deploymentName { - return &deployment, nil - } - - return nil, errTest -} - -func (repository *repositoryStub) GetValidDeployment(d string) (*common.Deployment, error) { - repository.GetValid = append(repository.GetValid, d) - return &deployment, nil -} - -func (repository *repositoryStub) CreateDeployment(d string) (*common.Deployment, error) { - repository.Created = append(repository.Created, d) - return &deployment, nil -} - -func (repository *repositoryStub) DeleteDeployment(d string, forget bool) (*common.Deployment, error) { - repository.Deleted = append(repository.Deleted, d) - return &deployment, nil -} - -func (repository *repositoryStub) SetDeploymentState(name string, state *common.DeploymentState) error { - repository.DeploymentStates = append(repository.DeploymentStates, state) - return nil -} - -// Manifests. -func (repository *repositoryStub) AddManifest(manifest *common.Manifest) error { - repository.ManifestAdd[manifest.Deployment] = manifest - return nil -} - -func (repository *repositoryStub) SetManifest(manifest *common.Manifest) error { - repository.ManifestSet[manifest.Deployment] = manifest - return nil -} - -func (repository *repositoryStub) ListManifests(d string) (map[string]*common.Manifest, error) { - if d == deploymentName { - return manifestMap, nil - } - - return nil, errTest -} - -func (repository *repositoryStub) GetManifest(d string, m string) (*common.Manifest, error) { - if d == deploymentName && m == manifestName { - return &manifest, nil - } - - return nil, errTest -} - -func (repository *repositoryStub) GetLatestManifest(d string) (*common.Manifest, error) { - if d == deploymentName { - return repository.ManifestAdd[d], nil - } - - return nil, errTest -} - -// Types. -func (repository *repositoryStub) ListCharts() ([]string, error) { - repository.ListTypesCalled = true - return []string{}, nil -} - -func (repository *repositoryStub) GetChartInstances(t string) ([]*common.ChartInstance, error) { - repository.GetChartInstancesCalled = true - return []*common.ChartInstance{}, nil -} - -func (repository *repositoryStub) ClearChartInstancesForDeployment(d string) error { - repository.ChartInstancesCleared = true - return nil -} - -func (repository *repositoryStub) AddChartInstances(is map[string][]*common.ChartInstance) error { - for t, instances := range is { - for _, instance := range instances { - d := instance.Deployment - repository.ChartInstances[d] = append(repository.ChartInstances[d], t) - } - } - - return nil -} - -func (repository *repositoryStub) Close() {} - -var testExpander = &expanderStub{} -var testRepository = newRepositoryStub() -var testDeployer = newDeployerStub() -var testRepoService = repo.NewInmemRepoService() -var testCredentialProvider = repo.NewInmemCredentialProvider() -var testProvider = repo.NewRepoProvider(nil, repo.NewGCSRepoProvider(testCredentialProvider), testCredentialProvider) -var testManager = NewManager(testExpander, testDeployer, testRepository, testProvider, testRepoService, testCredentialProvider) - -func TestListDeployments(t *testing.T) { - testRepository.reset() - d, err := testManager.ListDeployments() - if err != nil { - t.Fatalf(err.Error()) - } - - if !reflect.DeepEqual(d, deploymentList) { - t.Fatalf("invalid deployment list") - } -} - -func TestListDeploymentsFail(t *testing.T) { - testRepository.reset() - testRepository.FailListDeployments = true - d, err := testManager.ListDeployments() - if err != errTest { - t.Fatalf(err.Error()) - } - - if d != nil { - t.Fatalf("deployment list is not empty") - } -} - -func TestGetDeployment(t *testing.T) { - testRepository.reset() - d, err := testManager.GetDeployment(deploymentName) - if err != nil { - t.Fatalf(err.Error()) - } - - if !reflect.DeepEqual(d, &deployment) { - t.Fatalf("invalid deployment") - } -} - -func TestListManifests(t *testing.T) { - testRepository.reset() - m, err := testManager.ListManifests(deploymentName) - if err != nil { - t.Fatalf(err.Error()) - } - - if !reflect.DeepEqual(m, manifestMap) { - t.Fatalf("invalid manifest map") - } -} - -func TestGetManifest(t *testing.T) { - testRepository.reset() - m, err := testManager.GetManifest(deploymentName, manifestName) - if err != nil { - t.Fatalf(err.Error()) - } - - if !reflect.DeepEqual(m, &manifest) { - t.Fatalf("invalid manifest") - } -} - -func TestCreateDeployment(t *testing.T) { - testRepository.reset() - testDeployer.reset() - d, err := testManager.CreateDeployment(&deploymentRequest) - if !reflect.DeepEqual(d, &deployment) || err != nil { - t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+ - "Received: %v, %s. Expected: %#v, %s.", d, err, &deployment, "nil") - } - - if testRepository.Created[0] != deploymentRequest.Name { - t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", - testRepository.Created[0], deploymentRequest.Name) - } - - if !strings.HasPrefix(testRepository.ManifestAdd[deploymentRequest.Name].Name, "manifest-") { - t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ - "to begin with manifest-.", testRepository.ManifestAdd[deploymentRequest.Name].Name) - } - - if !strings.HasPrefix(testRepository.ManifestSet[deploymentRequest.Name].Name, "manifest-") { - t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+ - "to begin with manifest-.", testRepository.ManifestSet[deploymentRequest.Name].Name) - } - - if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil { - t.Fatalf("Deployer CreateConfiguration was called with %s but expected %s.", - testDeployer.Created[0], configuration) - } - - if testRepository.DeploymentStates[0].Status != common.DeployedStatus { - t.Fatal("CreateDeployment success did not mark deployment as deployed") - } - - if !testRepository.ChartInstancesCleared { - t.Fatal("Repository did not clear type instances during creation") - } - - if !reflect.DeepEqual(testRepository.ChartInstances, typeInstMap) { - t.Fatalf("Unexpected type instances after CreateDeployment: %s", testRepository.ChartInstances) - } -} - -func TestCreateDeploymentCreationFailure(t *testing.T) { - testRepository.reset() - testDeployer.reset() - testDeployer.FailCreate = true - d, err := testManager.CreateDeployment(&deploymentRequest) - - if testRepository.Created[0] != deploymentRequest.Name { - t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", - testRepository.Created[0], deploymentRequest.Name) - } - - if len(testRepository.Deleted) != 0 { - t.Fatalf("DeleteDeployment was called with %s but not expected", - testRepository.Created[0]) - } - - if testRepository.DeploymentStates[0].Status != common.FailedStatus { - t.Fatal("CreateDeployment failure did not mark deployment as failed") - } - - if err != errTest || d != nil { - t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+ - "Received: %v, %s. Expected: %s, %s.", d, err, "nil", errTest) - } - - if testRepository.ChartInstancesCleared { - t.Fatal("Unexpected change to type instances during CreateDeployment failure.") - } -} - -func TestCreateDeploymentCreationResourceFailure(t *testing.T) { - testRepository.reset() - testDeployer.reset() - testDeployer.FailCreateResource = true - d, err := testManager.CreateDeployment(&deploymentRequest) - - if testRepository.Created[0] != deploymentRequest.Name { - t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", - testRepository.Created[0], deploymentRequest.Name) - } - - if len(testRepository.Deleted) != 0 { - t.Fatalf("DeleteDeployment was called with %s but not expected", - testRepository.Created[0]) - } - - if testRepository.DeploymentStates[0].Status != common.FailedStatus { - t.Fatal("CreateDeployment failure did not mark deployment as failed") - } - - if manifest, ok := testRepository.ManifestAdd[deploymentRequest.Name]; ok { - if !strings.HasPrefix(manifest.Name, "manifest-") { - t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ - "to begin with manifest-.", manifest.Name) - } - } - - if manifest, ok := testRepository.ManifestSet[deploymentRequest.Name]; ok { - if !strings.HasPrefix(manifest.Name, "manifest-") { - t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ - "to begin with manifest-.", manifest.Name) - } - } - - if err != nil || !reflect.DeepEqual(d, &deployment) { - t.Fatalf("Expected a different set of response values from invoking CreateDeployment.\n"+ - "Received: %v, %v. Expected: %v, %v.", d, err, &deployment, "nil") - } - - if !testRepository.ChartInstancesCleared { - t.Fatal("Repository did not clear type instances during creation") - } -} - -func TestDeleteDeploymentForget(t *testing.T) { - testRepository.reset() - testDeployer.reset() - d, err := testManager.CreateDeployment(&deploymentRequest) - if !reflect.DeepEqual(d, &deployment) || err != nil { - t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+ - "Received: %v, %s. Expected: %#v, %s.", d, err, &deployment, "nil") - } - - if testRepository.Created[0] != deploymentRequest.Name { - t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", - testRepository.Created[0], deploymentRequest.Name) - } - - if !strings.HasPrefix(testRepository.ManifestAdd[deploymentRequest.Name].Name, "manifest-") { - t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ - "to begin with manifest-.", testRepository.ManifestAdd[deploymentRequest.Name].Name) - } - - if !strings.HasPrefix(testRepository.ManifestSet[deploymentRequest.Name].Name, "manifest-") { - t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+ - "to begin with manifest-.", testRepository.ManifestSet[deploymentRequest.Name].Name) - } - - if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil { - t.Fatalf("Deployer CreateConfiguration was called with %s but expected %s.", - testDeployer.Created[0], configuration) - } - d, err = testManager.DeleteDeployment(deploymentName, true) - if err != nil { - t.Fatalf("DeleteDeployment failed with %v", err) - } - - // Make sure the resources were deleted through deployer. - if len(testDeployer.Deleted) > 0 { - c := testDeployer.Deleted[0] - if c != nil { - if !reflect.DeepEqual(*c, configuration) || err != nil { - t.Fatalf("Deployer DeleteConfiguration was called with %s but expected %s.", - testDeployer.Created[0], configuration) - } - } - } - - if !testRepository.ChartInstancesCleared { - t.Fatal("Expected type instances to be cleared during DeleteDeployment.") - } -} - -func TestExpand(t *testing.T) { - m, err := testManager.Expand(&deploymentRequest) - if err != nil { - t.Fatal("Failed to expand deployment request into manifest.") - } - - if !reflect.DeepEqual(*m.ExpandedConfig, configuration) { - t.Fatalf("Expanded config not correct in output manifest: %v", *m) - } - - if !reflect.DeepEqual(*m.Layout, layout) { - t.Fatalf("Layout not correct in output manifest: %v", *m) - } -} - -func TestListTypes(t *testing.T) { - testRepository.reset() - - testManager.ListCharts() - - if !testRepository.ListTypesCalled { - t.Fatal("expected repository ListCharts() call.") - } -} - -func TestListInstances(t *testing.T) { - testRepository.reset() - - testManager.ListChartInstances("all") - - if !testRepository.GetChartInstancesCalled { - t.Fatal("expected repository GetChartInstances() call.") - } -} - -// TODO(jackgr): Implement TestListRepoCharts -func TestListRepoCharts(t *testing.T) { - /* - types, err := testManager.ListRepoCharts("", nil) - if err != nil { - t.Fatalf("cannot list repository types: %s", err) - } - */ -} - -// TODO(jackgr): Implement TestGetDownloadURLs -func TestGetDownloadURLs(t *testing.T) { - /* - urls, err := testManager.GetDownloadURLs("", repo.Type{}) - if err != nil { - t.Fatalf("cannot list get download urls: %s", err) - } - */ -} diff --git a/cmd/manager/repository/persistent/persistent.go b/cmd/manager/repository/persistent/persistent.go deleted file mode 100644 index 06e6abb0c..000000000 --- a/cmd/manager/repository/persistent/persistent.go +++ /dev/null @@ -1,488 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. -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. -*/ - -// Package persistent implements a persistent deployment repository. -// -// This package is currently implemented using MondoDB, but there is no -// guarantee that it will continue to be implemented using MondoDB in the -// future. -package persistent - -import ( - "fmt" - "log" - "net/url" - "os" - "time" - - "github.com/kubernetes/helm/cmd/manager/repository" - "github.com/kubernetes/helm/pkg/common" - - "gopkg.in/mgo.v2" - "gopkg.in/mgo.v2/bson" -) - -type pDeployment struct { - ID string `bson:"_id"` - common.Deployment -} - -type pManifest struct { - ID string `bson:"_id"` - common.Manifest -} - -type pInstance struct { - ID string `bson:"_id"` - common.ChartInstance -} - -type pRepository struct { - Session *mgo.Session // mongodb session - Deployments *mgo.Collection // deployments collection - Manifests *mgo.Collection // manifests collection - Instances *mgo.Collection // instances collection -} - -// Constants used to configure the MongoDB database. -const ( - DatabaseName = "deployment_manager" - DeploymentsCollectionName = "deployments_collection" - ManifestsCollectionName = "manifests_collection" - InstancesCollectionName = "instances_collection" -) - -// NewRepository returns a new persistent repository. Its lifetime is decopuled -// from the lifetime of the current process. When the process dies, its contents -// will not be affected. -// -// The server argument provides connection information for the repository server. -// It is parsed as a URL, and the username, password, host and port, if provided, -// are used to create the connection string. -func NewRepository(server string) (repository.Repository, error) { - travis := os.Getenv("TRAVIS") - if travis == "true" { - err := fmt.Errorf("cannot use MongoDB in Travis CI due to gopkg.in/mgo.v2 issue #218") - log.Println(err.Error()) - return nil, err - } - - u, err := url.Parse(server) - if err != nil { - err2 := fmt.Errorf("cannot parse url '%s': %s\n", server, err) - log.Println(err2.Error()) - return nil, err2 - } - - u2 := &url.URL{Scheme: "mongodb", User: u.User, Host: u.Host} - server = u2.String() - - session, err := mgo.Dial(server) - if err != nil { - err2 := fmt.Errorf("cannot connect to MongoDB at %s: %s\n", server, err) - log.Println(err2.Error()) - return nil, err2 - } - - session.SetMode(mgo.Strong, false) - session.SetSafe(&mgo.Safe{WMode: "majority"}) - database := session.DB(DatabaseName) - deployments, err := createCollection(database, DeploymentsCollectionName, nil) - if err != nil { - return nil, err - } - - manifests, err := createCollection(database, ManifestsCollectionName, - [][]string{{"manifest.deployment"}}) - if err != nil { - return nil, err - } - - instances, err := createCollection(database, InstancesCollectionName, - [][]string{{"typeinstance.type"}, {"typeinstance.deployment"}}) - if err != nil { - return nil, err - } - - pr := &pRepository{ - Session: session, - Deployments: deployments, - Manifests: manifests, - Instances: instances, - } - - return pr, nil -} - -func createCollection(db *mgo.Database, cName string, keys [][]string) (*mgo.Collection, error) { - c := db.C(cName) - for _, key := range keys { - if err := createIndex(c, key...); err != nil { - return nil, err - } - } - - return c, nil -} - -func createIndex(c *mgo.Collection, key ...string) error { - if err := c.EnsureIndexKey(key...); err != nil { - err2 := fmt.Errorf("cannot create index %v for collection %s: %s\n", key, c.Name, err) - log.Println(err2.Error()) - return err2 - } - - return nil -} - -// Reset returns the repository to its initial state. -func (r *pRepository) Reset() error { - database := r.Session.DB(DatabaseName) - if err := database.DropDatabase(); err != nil { - return fmt.Errorf("cannot drop database %s", database.Name) - } - - r.Close() - return nil -} - -// Close cleans up any resources used by the repository. -func (r *pRepository) Close() { - r.Session.Close() -} - -// ListDeployments returns of all of the deployments in the repository. -func (r *pRepository) ListDeployments() ([]common.Deployment, error) { - var result []pDeployment - if err := r.Deployments.Find(nil).All(&result); err != nil { - return nil, fmt.Errorf("cannot list deployments: %s", err) - } - - deployments := []common.Deployment{} - for _, pd := range result { - deployments = append(deployments, pd.Deployment) - } - - return deployments, nil -} - -// GetDeployment returns the deployment with the supplied name. -// If the deployment is not found, it returns an error. -func (r *pRepository) GetDeployment(name string) (*common.Deployment, error) { - result := pDeployment{} - if err := r.Deployments.FindId(name).One(&result); err != nil { - return nil, fmt.Errorf("cannot get deployment %s: %s", name, err) - } - - return &result.Deployment, nil -} - -// GetValidDeployment returns the deployment with the supplied name. -// If the deployment is not found or marked as deleted, it returns an error. -func (r *pRepository) GetValidDeployment(name string) (*common.Deployment, error) { - d, err := r.GetDeployment(name) - if err != nil { - return nil, err - } - - if d.State.Status == common.DeletedStatus { - return nil, fmt.Errorf("deployment %s is deleted", name) - } - - return d, nil -} - -// CreateDeployment creates a new deployment and stores it in the repository. -func (r *pRepository) CreateDeployment(name string) (*common.Deployment, error) { - exists, _ := r.GetValidDeployment(name) - if exists != nil { - return nil, fmt.Errorf("deployment %s already exists", name) - } - - d := common.NewDeployment(name) - if err := r.insertDeployment(d); err != nil { - return nil, err - } - - log.Printf("created deployment: %v", d) - return d, nil -} - -// SetDeploymentStatus sets the DeploymentStatus of the deployment and updates ModifiedAt -func (r *pRepository) SetDeploymentState(name string, state *common.DeploymentState) error { - d, err := r.GetValidDeployment(name) - if err != nil { - return err - } - - d.State = state - return r.updateDeployment(d) -} - -func (r *pRepository) AddManifest(manifest *common.Manifest) error { - deploymentName := manifest.Deployment - d, err := r.GetValidDeployment(deploymentName) - if err != nil { - return err - } - - count, err := r.Manifests.FindId(manifest.Name).Count() - if err != nil { - return fmt.Errorf("cannot search for manifest %s: %s", manifest.Name, err) - } - - if count > 0 { - return fmt.Errorf("manifest %s already exists", manifest.Name) - } - - if err := r.insertManifest(manifest); err != nil { - return err - } - - d.LatestManifest = manifest.Name - if err := r.updateDeployment(d); err != nil { - return err - } - - log.Printf("Added manifest %s to deployment: %s", manifest.Name, deploymentName) - return nil -} - -// DeleteDeployment deletes the deployment with the supplied name. -// If forget is true, then the deployment is removed from the repository. -// Otherwise, it is marked as deleted and retained. -func (r *pRepository) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { - d, err := r.GetValidDeployment(name) - if err != nil { - return nil, err - } - - if !forget { - d.DeletedAt = time.Now() - d.State = &common.DeploymentState{Status: common.DeletedStatus} - if err := r.updateDeployment(d); err != nil { - return nil, err - } - } else { - d.LatestManifest = "" - if err := r.removeManifestsForDeployment(d); err != nil { - return nil, err - } - - if err := r.removeDeployment(d); err != nil { - return nil, err - } - } - - log.Printf("deleted deployment: %v", d) - return d, nil -} - -func (r *pRepository) insertDeployment(d *common.Deployment) error { - if d != nil && d.Name != "" { - wrapper := pDeployment{ID: d.Name, Deployment: *d} - if err := r.Deployments.Insert(&wrapper); err != nil { - return fmt.Errorf("cannot insert deployment %v: %s", wrapper, err) - } - } - - return nil -} - -func (r *pRepository) removeDeployment(d *common.Deployment) error { - if d != nil && d.Name != "" { - if err := r.Deployments.RemoveId(d.Name); err != nil { - return fmt.Errorf("cannot remove deployment %s: %s", d.Name, err) - } - } - - return nil -} - -func (r *pRepository) updateDeployment(d *common.Deployment) error { - if d != nil && d.Name != "" { - if d.State.Status != common.DeletedStatus { - d.ModifiedAt = time.Now() - } - - wrapper := pDeployment{ID: d.Name, Deployment: *d} - if err := r.Deployments.UpdateId(d.Name, &wrapper); err != nil { - return fmt.Errorf("cannot update deployment %v: %s", wrapper, err) - } - } - - return nil -} - -func (r *pRepository) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { - _, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.listManifestsForDeployment(deploymentName) -} - -func (r *pRepository) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) { - _, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.getManifestForDeployment(deploymentName, manifestName) -} - -// GetLatestManifest returns the latest manifest for a given deployment, -// which by definition is the manifest with the largest time stamp. -func (r *pRepository) GetLatestManifest(deploymentName string) (*common.Manifest, error) { - d, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - if d.LatestManifest == "" { - return nil, nil - } - - return r.getManifestForDeployment(deploymentName, d.LatestManifest) -} - -// SetManifest sets an existing manifest in the repository to provided manifest. -func (r *pRepository) SetManifest(manifest *common.Manifest) error { - _, err := r.GetManifest(manifest.Deployment, manifest.Name) - if err != nil { - return err - } - - return r.updateManifest(manifest) -} - -func (r *pRepository) updateManifest(m *common.Manifest) error { - if m != nil && m.Name != "" { - wrapper := pManifest{ID: m.Name, Manifest: *m} - if err := r.Manifests.UpdateId(m.Name, &wrapper); err != nil { - return fmt.Errorf("cannot update manifest %v: %s", wrapper, err) - } - } - - return nil -} - -func (r *pRepository) listManifestsForDeployment(deploymentName string) (map[string]*common.Manifest, error) { - query := bson.M{"manifest.deployment": deploymentName} - var result []pManifest - if err := r.Manifests.Find(query).All(&result); err != nil { - return nil, fmt.Errorf("cannot list manifests for deployment %s: %s", deploymentName, err) - } - - l := make(map[string]*common.Manifest, 0) - for _, pm := range result { - l[pm.Name] = &pm.Manifest - } - - return l, nil -} - -func (r *pRepository) getManifestForDeployment(deploymentName string, manifestName string) (*common.Manifest, error) { - result := pManifest{} - if err := r.Manifests.FindId(manifestName).One(&result); err != nil { - return nil, fmt.Errorf("cannot get manifest %s: %s", manifestName, err) - } - - if result.Deployment != deploymentName { - return nil, fmt.Errorf("manifest %s not found in deployment %s", manifestName, deploymentName) - } - - return &result.Manifest, nil -} - -func (r *pRepository) insertManifest(m *common.Manifest) error { - if m != nil && m.Name != "" { - wrapper := pManifest{ID: m.Name, Manifest: *m} - if err := r.Manifests.Insert(&wrapper); err != nil { - return fmt.Errorf("cannot insert manifest %v: %s", wrapper, err) - } - } - - return nil -} - -func (r *pRepository) removeManifestsForDeployment(d *common.Deployment) error { - if d != nil && d.Name != "" { - query := bson.M{"manifest.deployment": d.Name} - if _, err := r.Manifests.RemoveAll(query); err != nil { - return fmt.Errorf("cannot remove all manifests for deployment %s: %s", d.Name, err) - } - } - - return nil -} - -// ListCharts returns all types known from existing instances. -func (r *pRepository) ListCharts() ([]string, error) { - var result []string - if err := r.Instances.Find(nil).Distinct("typeinstance.type", &result); err != nil { - return nil, fmt.Errorf("cannot list type instances: %s", err) - } - - return result, nil -} - -// GetChartInstances returns all instances of a given type. If typeName is empty -// or equal to "all", returns all instances of all types. -func (r *pRepository) GetChartInstances(typeName string) ([]*common.ChartInstance, error) { - query := bson.M{"typeinstance.type": typeName} - if typeName == "" || typeName == "all" { - query = nil - } - - var result []pInstance - if err := r.Instances.Find(query).All(&result); err != nil { - return nil, fmt.Errorf("cannot get instances of type %s: %s", typeName, err) - } - - instances := []*common.ChartInstance{} - for _, pi := range result { - instances = append(instances, &pi.ChartInstance) - } - - return instances, nil -} - -// ClearChartInstancesForDeployment deletes all type instances associated with the given -// deployment from the repository. -func (r *pRepository) ClearChartInstancesForDeployment(deploymentName string) error { - if deploymentName != "" { - query := bson.M{"typeinstance.deployment": deploymentName} - if _, err := r.Instances.RemoveAll(query); err != nil { - return fmt.Errorf("cannot clear type instances for deployment %s: %s", deploymentName, err) - } - } - - return nil -} - -// AddChartInstances adds the supplied type instances to the repository. -func (r *pRepository) AddChartInstances(instances map[string][]*common.ChartInstance) error { - for _, is := range instances { - for _, i := range is { - key := fmt.Sprintf("%s.%s.%s", i.Deployment, i.Type, i.Name) - wrapper := pInstance{ID: key, ChartInstance: *i} - if err := r.Instances.Insert(&wrapper); err != nil { - return fmt.Errorf("cannot insert type instance %v: %s", wrapper, err) - } - } - } - - return nil -} diff --git a/cmd/manager/repository/persistent/persistent_test.go b/cmd/manager/repository/persistent/persistent_test.go deleted file mode 100644 index 4581c22bc..000000000 --- a/cmd/manager/repository/persistent/persistent_test.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. -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. -*/ - -package persistent - -import ( - "github.com/kubernetes/helm/cmd/manager/repository" - - "sync" - "testing" -) - -var tryRepository = true -var repositoryLock sync.RWMutex - -func createRepository() repository.Repository { - repositoryLock.Lock() - defer repositoryLock.Unlock() - - if tryRepository { - r, err := NewRepository("mongodb://localhost") - if err == nil { - return r - } - } - - tryRepository = false - return nil -} - -func resetRepository(t *testing.T, r repository.Repository) { - if r != nil { - if err := r.(*pRepository).Reset(); err != nil { - t.Fatalf("cannot reset repository: %s\n", err) - } - } -} - -func TestRepositoryListEmpty(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryListEmpty(t, r) - } -} - -func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryGetFailsWithNonExistentDeployment(t, r) - } -} - -func TestRepositoryCreateDeploymentWorks(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryCreateDeploymentWorks(t, r) - } -} - -func TestRepositoryMultipleManifestsWorks(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryMultipleManifestsWorks(t, r) - } -} - -func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryDeleteFailsWithNonExistentDeployment(t, r) - } -} - -func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryDeleteWorksWithNoLatestManifest(t, r) - } -} - -func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryDeleteDeploymentWorksNoForget(t, r) - } -} - -func TestRepositoryDeleteDeploymentWorksForget(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryDeleteDeploymentWorksForget(t, r) - } -} - -func TestRepositoryChartInstances(t *testing.T) { - if r := createRepository(); r != nil { - defer resetRepository(t, r) - repository.TestRepositoryChartInstances(t, r) - } -} diff --git a/cmd/manager/repository/repository.go b/cmd/manager/repository/repository.go deleted file mode 100644 index 40ebaee37..000000000 --- a/cmd/manager/repository/repository.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -// Package repository defines a deployment repository. -package repository - -import ( - "github.com/kubernetes/helm/pkg/common" -) - -// Repository manages storage for all Helm entities, as well as -// the common operations to store, access and manage them. -type Repository interface { - // Deployments. - ListDeployments() ([]common.Deployment, error) - GetDeployment(name string) (*common.Deployment, error) - GetValidDeployment(name string) (*common.Deployment, error) - CreateDeployment(name string) (*common.Deployment, error) - DeleteDeployment(name string, forget bool) (*common.Deployment, error) - SetDeploymentState(name string, state *common.DeploymentState) error - - // Manifests. - AddManifest(manifest *common.Manifest) error - SetManifest(manifest *common.Manifest) error - ListManifests(deploymentName string) (map[string]*common.Manifest, error) - GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) - GetLatestManifest(deploymentName string) (*common.Manifest, error) - - // Types. - ListCharts() ([]string, error) - GetChartInstances(chartName string) ([]*common.ChartInstance, error) - ClearChartInstancesForDeployment(deploymentName string) error - AddChartInstances(instances map[string][]*common.ChartInstance) error - - Close() -} diff --git a/cmd/manager/repository/test_common.go b/cmd/manager/repository/test_common.go deleted file mode 100644 index 60efbe40f..000000000 --- a/cmd/manager/repository/test_common.go +++ /dev/null @@ -1,340 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repository - -import ( - "github.com/kubernetes/helm/pkg/common" - - "fmt" - "testing" -) - -// TestRepositoryListEmpty checks that listing an empty repository works. -func TestRepositoryListEmpty(t *testing.T, r Repository) { - d, err := r.ListDeployments() - if err != nil { - t.Fatal("List Deployments failed") - } - - if len(d) != 0 { - t.Fatal("Returned non zero list") - } -} - -// TestRepositoryGetFailsWithNonExistentDeployment checks that getting a non-existent deployment fails. -func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T, r Repository) { - _, err := r.GetDeployment("nothere") - if err == nil { - t.Fatal("GetDeployment didn't fail with non-existent deployment") - } -} - -func testCreateDeploymentWithManifests(t *testing.T, r Repository, count int) { - var deploymentName = "mydeployment" - - d, err := r.CreateDeployment(deploymentName) - if err != nil { - t.Fatalf("CreateDeployment failed: %v", err) - } - - l, err := r.ListDeployments() - if err != nil { - t.Fatalf("ListDeployments failed: %v", err) - } - - if len(l) != 1 { - t.Fatalf("Number of deployments listed is not 1: %d", len(l)) - } - - dNew, err := r.GetDeployment(deploymentName) - if err != nil { - t.Fatalf("GetDeployment failed: %v", err) - } - - if dNew.Name != d.Name { - t.Fatalf("Deployment Names don't match, got: %v, expected %v", dNew, d) - } - - mList, err := r.ListManifests(deploymentName) - if err != nil { - t.Fatalf("ListManifests failed: %v", err) - } - - if len(mList) != 0 { - t.Fatalf("Deployment has non-zero manifest count: %d", len(mList)) - } - - for i := 0; i < count; i++ { - var manifestName = fmt.Sprintf("manifest-%d", i) - manifest := common.Manifest{Deployment: deploymentName, Name: manifestName} - err := r.AddManifest(&manifest) - if err != nil { - t.Fatalf("AddManifest failed: %v", err) - } - - d, err = r.GetDeployment(deploymentName) - if err != nil { - t.Fatalf("GetDeployment failed: %v", err) - } - - if d.LatestManifest != manifestName { - t.Fatalf("AddManifest did not update LatestManifest: %#v", d) - } - - mListNew, err := r.ListManifests(deploymentName) - if err != nil { - t.Fatalf("ListManifests failed: %v", err) - } - - if len(mListNew) != i+1 { - t.Fatalf("Deployment has unexpected manifest count: want %d, have %d", i+1, len(mListNew)) - } - - m, err := r.GetManifest(deploymentName, manifestName) - if err != nil { - t.Fatalf("GetManifest failed: %v", err) - } - - if m.Name != manifestName { - t.Fatalf("Unexpected manifest name: want %s, have %s", manifestName, m.Name) - } - } -} - -// TestRepositoryCreateDeploymentWorks checks that creating a deployment works. -func TestRepositoryCreateDeploymentWorks(t *testing.T, r Repository) { - testCreateDeploymentWithManifests(t, r, 1) -} - -// TestRepositoryMultipleManifestsWorks checks that creating a deploymente with multiple manifests works. -func TestRepositoryMultipleManifestsWorks(t *testing.T, r Repository) { - testCreateDeploymentWithManifests(t, r, 7) -} - -// TestRepositoryDeleteFailsWithNonExistentDeployment checks that deleting a non-existent deployment fails. -func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T, r Repository) { - var deploymentName = "mydeployment" - d, err := r.DeleteDeployment(deploymentName, false) - if err == nil { - t.Fatalf("DeleteDeployment didn't fail with non existent deployment") - } - - if d != nil { - t.Fatalf("DeleteDeployment returned non-nil for non existent deployment") - } -} - -// TestRepositoryDeleteWorksWithNoLatestManifest checks that deleting a deployment with no latest manifest works. -func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T, r Repository) { - var deploymentName = "mydeployment" - _, err := r.CreateDeployment(deploymentName) - if err != nil { - t.Fatalf("CreateDeployment failed: %v", err) - } - - dDeleted, err := r.DeleteDeployment(deploymentName, false) - if err != nil { - t.Fatalf("DeleteDeployment failed: %v", err) - } - - if dDeleted.State.Status != common.DeletedStatus { - t.Fatalf("Deployment Status is not deleted") - } - - if _, err := r.ListManifests(deploymentName); err == nil { - t.Fatalf("Manifests are not deleted") - } -} - -// TestRepositoryDeleteDeploymentWorksNoForget checks that deleting a deployment without forgetting it works. -func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T, r Repository) { - var deploymentName = "mydeployment" - var manifestName = "manifest-0" - manifest := common.Manifest{Deployment: deploymentName, Name: manifestName} - _, err := r.CreateDeployment(deploymentName) - if err != nil { - t.Fatalf("CreateDeployment failed: %v", err) - } - - err = r.AddManifest(&manifest) - if err != nil { - t.Fatalf("AddManifest failed: %v", err) - } - - dDeleted, err := r.DeleteDeployment(deploymentName, false) - if err != nil { - t.Fatalf("DeleteDeployment failed: %v", err) - } - - if dDeleted.State.Status != common.DeletedStatus { - t.Fatalf("Deployment Status is not deleted") - } -} - -// TestRepositoryDeleteDeploymentWorksForget checks that deleting and forgetting a deployment works. -func TestRepositoryDeleteDeploymentWorksForget(t *testing.T, r Repository) { - var deploymentName = "mydeployment" - var manifestName = "manifest-0" - manifest := common.Manifest{Deployment: deploymentName, Name: manifestName} - _, err := r.CreateDeployment(deploymentName) - if err != nil { - t.Fatalf("CreateDeployment failed: %v", err) - } - - err = r.AddManifest(&manifest) - if err != nil { - t.Fatalf("AddManifest failed: %v", err) - } - - dDeleted, err := r.DeleteDeployment(deploymentName, true) - if err != nil { - t.Fatalf("DeleteDeployment failed: %v", err) - } - - if dDeleted.State.Status != common.CreatedStatus { - t.Fatalf("Deployment Status is not created") - } -} - -// TestRepositoryChartInstances checks that type instances can be listed and retrieved successfully. -func TestRepositoryChartInstances(t *testing.T, r Repository) { - d1Map := map[string][]*common.ChartInstance{ - "t1": { - { - Name: "i1", - Type: "t1", - Deployment: "d1", - Manifest: "m1", - Path: "p1", - }, - }, - } - - d2Map := map[string][]*common.ChartInstance{ - "t2": { - { - Name: "i2", - Type: "t2", - Deployment: "d2", - Manifest: "m2", - Path: "p2", - }, - }, - } - - d3Map := map[string][]*common.ChartInstance{ - "t2": { - { - Name: "i3", - Type: "t2", - Deployment: "d3", - Manifest: "m3", - Path: "p3", - }, - }, - } - - instances, err := r.GetChartInstances("noinstances") - if err != nil { - t.Fatal(err) - } - - if len(instances) != 0 { - t.Fatalf("expected no instances: %v", instances) - } - - types, err := r.ListCharts() - if err != nil { - t.Fatal(err) - } - - if len(types) != 0 { - t.Fatalf("expected no types: %v", types) - } - - r.AddChartInstances(d1Map) - r.AddChartInstances(d2Map) - r.AddChartInstances(d3Map) - - instances, err = r.GetChartInstances("unknowntype") - if err != nil { - t.Fatal(err) - } - - if len(instances) != 0 { - t.Fatalf("expected no instances: %v", instances) - } - - instances, err = r.GetChartInstances("t1") - if err != nil { - t.Fatal(err) - } - - if len(instances) != 1 { - t.Fatalf("expected one instance: %v", instances) - } - - instances, err = r.GetChartInstances("t2") - if err != nil { - t.Fatal(err) - } - - if len(instances) != 2 { - t.Fatalf("expected two instances: %v", instances) - } - - instances, err = r.GetChartInstances("all") - if err != nil { - t.Fatal(err) - } - - if len(instances) != 3 { - t.Fatalf("expected three total instances: %v", instances) - } - - types, err = r.ListCharts() - if err != nil { - t.Fatal(err) - } - - if len(types) != 2 { - t.Fatalf("expected two total types: %v", types) - } - - err = r.ClearChartInstancesForDeployment("d1") - if err != nil { - t.Fatal(err) - } - - instances, err = r.GetChartInstances("t1") - if err != nil { - t.Fatal(err) - } - - if len(instances) != 0 { - t.Fatalf("expected no instances after clear: %v", instances) - } - - types, err = r.ListCharts() - if err != nil { - t.Fatal(err) - } - - if len(types) != 1 { - t.Fatalf("expected one total type: %v", types) - } -} diff --git a/cmd/manager/repository/transient/transient.go b/cmd/manager/repository/transient/transient.go deleted file mode 100644 index 774d26e75..000000000 --- a/cmd/manager/repository/transient/transient.go +++ /dev/null @@ -1,325 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -// Package transient implements a transient deployment repository. -package transient - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/kubernetes/helm/cmd/manager/repository" - "github.com/kubernetes/helm/pkg/common" -) - -// deploymentChartInstanceMap stores type instances mapped by deployment name. -// This allows for simple updating and deleting of per-deployment instances -// when deployments are created/updated/deleted. -type deploymentChartInstanceMap map[string][]*common.ChartInstance - -type tRepository struct { - sync.RWMutex - deployments map[string]common.Deployment - manifests map[string]map[string]*common.Manifest - instances map[string]deploymentChartInstanceMap -} - -// NewRepository returns a new transient repository. Its lifetime is coupled -// to the lifetime of the current process. When the process dies, its contents -// will be permanently destroyed. -func NewRepository() repository.Repository { - return &tRepository{ - deployments: make(map[string]common.Deployment, 0), - manifests: make(map[string]map[string]*common.Manifest, 0), - instances: make(map[string]deploymentChartInstanceMap, 0), - } -} - -func (r *tRepository) Close() { - r.deployments = make(map[string]common.Deployment, 0) - r.manifests = make(map[string]map[string]*common.Manifest, 0) - r.instances = make(map[string]deploymentChartInstanceMap, 0) -} - -// ListDeployments returns of all of the deployments in the repository. -func (r *tRepository) ListDeployments() ([]common.Deployment, error) { - l := []common.Deployment{} - for _, deployment := range r.deployments { - l = append(l, deployment) - } - - return l, nil -} - -// GetDeployment returns the deployment with the supplied name. -// If the deployment is not found, it returns an error. -func (r *tRepository) GetDeployment(name string) (*common.Deployment, error) { - d, ok := r.deployments[name] - if !ok { - return nil, fmt.Errorf("deployment %s not found", name) - } - - return &d, nil -} - -// GetValidDeployment returns the deployment with the supplied name. -// If the deployment is not found or marked as deleted, it returns an error. -func (r *tRepository) GetValidDeployment(name string) (*common.Deployment, error) { - d, err := r.GetDeployment(name) - if err != nil { - return nil, err - } - - if d.State.Status == common.DeletedStatus { - return nil, fmt.Errorf("deployment %s is deleted", name) - } - - return d, nil -} - -// SetDeploymentState sets the DeploymentState of the deployment and updates ModifiedAt -func (r *tRepository) SetDeploymentState(name string, state *common.DeploymentState) error { - r.Lock() - defer r.Unlock() - - d, err := r.GetValidDeployment(name) - if err != nil { - return err - } - - d.State = state - d.ModifiedAt = time.Now() - r.deployments[name] = *d - return nil -} - -// CreateDeployment creates a new deployment and stores it in the repository. -func (r *tRepository) CreateDeployment(name string) (*common.Deployment, error) { - r.Lock() - defer r.Unlock() - - exists, _ := r.GetValidDeployment(name) - if exists != nil { - return nil, fmt.Errorf("Deployment %s already exists", name) - } - - d := common.NewDeployment(name) - r.deployments[name] = *d - - log.Printf("created deployment: %v", d) - return d, nil -} - -// AddManifest adds a manifest to the repository and repoints the latest -// manifest to it for the corresponding deployment. -func (r *tRepository) AddManifest(manifest *common.Manifest) error { - r.Lock() - defer r.Unlock() - - deploymentName := manifest.Deployment - l, err := r.ListManifests(deploymentName) - if err != nil { - return err - } - - // Make sure the manifest doesn't already exist, and if not, add the manifest to - // map of manifests this deployment has - if _, ok := l[manifest.Name]; ok { - return fmt.Errorf("Manifest %s already exists in deployment %s", manifest.Name, deploymentName) - } - - d, err := r.GetValidDeployment(deploymentName) - if err != nil { - return err - } - - l[manifest.Name] = manifest - d.LatestManifest = manifest.Name - d.ModifiedAt = time.Now() - r.deployments[deploymentName] = *d - - log.Printf("Added manifest %s to deployment: %s", manifest.Name, deploymentName) - return nil -} - -// SetManifest sets an existing manifest in the repository to provided manifest. -func (r *tRepository) SetManifest(manifest *common.Manifest) error { - r.Lock() - defer r.Unlock() - - l, err := r.ListManifests(manifest.Deployment) - if err != nil { - return err - } - - if _, ok := l[manifest.Name]; !ok { - return fmt.Errorf("manifest %s not found", manifest.Name) - } - - l[manifest.Name] = manifest - return nil -} - -// DeleteDeployment deletes the deployment with the supplied name. -// If forget is true, then the deployment is removed from the repository. -// Otherwise, it is marked as deleted and retained. -func (r *tRepository) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { - r.Lock() - defer r.Unlock() - - d, err := r.GetValidDeployment(name) - if err != nil { - return nil, err - } - - if !forget { - d.DeletedAt = time.Now() - d.State = &common.DeploymentState{Status: common.DeletedStatus} - r.deployments[name] = *d - } else { - delete(r.deployments, name) - delete(r.manifests, name) - d.LatestManifest = "" - } - - log.Printf("deleted deployment: %v", d) - return d, nil -} - -func (r *tRepository) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { - _, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.listManifestsForDeployment(deploymentName) -} - -func (r *tRepository) listManifestsForDeployment(deploymentName string) (map[string]*common.Manifest, error) { - l, ok := r.manifests[deploymentName] - if !ok { - l = make(map[string]*common.Manifest, 0) - r.manifests[deploymentName] = l - } - - return l, nil -} - -func (r *tRepository) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) { - _, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.getManifestForDeployment(deploymentName, manifestName) -} - -func (r *tRepository) getManifestForDeployment(deploymentName string, manifestName string) (*common.Manifest, error) { - l, err := r.listManifestsForDeployment(deploymentName) - if err != nil { - return nil, err - } - - m, ok := l[manifestName] - if !ok { - return nil, fmt.Errorf("manifest %s not found in deployment %s", manifestName, deploymentName) - } - - return m, nil -} - -// GetLatestManifest returns the latest manifest for a given deployment, -// which by definition is the manifest with the largest time stamp. -func (r *tRepository) GetLatestManifest(deploymentName string) (*common.Manifest, error) { - d, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - if d.LatestManifest == "" { - return nil, nil - } - - return r.getManifestForDeployment(deploymentName, d.LatestManifest) -} - -// ListCharts returns all types known from existing instances. -func (r *tRepository) ListCharts() ([]string, error) { - var keys []string - for k := range r.instances { - keys = append(keys, k) - } - - return keys, nil -} - -// GetChartInstances returns all instances of a given type. If type is empty, -// returns all instances for all types. -func (r *tRepository) GetChartInstances(typeName string) ([]*common.ChartInstance, error) { - var instances []*common.ChartInstance - for t, dInstMap := range r.instances { - if t == typeName || typeName == "" || typeName == "all" { - for _, i := range dInstMap { - instances = append(instances, i...) - } - } - } - - return instances, nil -} - -// ClearChartInstancesForDeployment deletes all type instances associated with the given -// deployment from the repository. -func (r *tRepository) ClearChartInstancesForDeployment(deploymentName string) error { - r.Lock() - defer r.Unlock() - - for t, dMap := range r.instances { - delete(dMap, deploymentName) - if len(dMap) == 0 { - delete(r.instances, t) - } - } - - return nil -} - -// AddChartInstances adds the supplied type instances to the repository. -func (r *tRepository) AddChartInstances(instances map[string][]*common.ChartInstance) error { - r.Lock() - defer r.Unlock() - - // Add instances to the appropriate type and deployment maps. - for t, is := range instances { - if r.instances[t] == nil { - r.instances[t] = make(deploymentChartInstanceMap) - } - - tmap := r.instances[t] - for _, instance := range is { - deployment := instance.Deployment - if tmap[deployment] == nil { - tmap[deployment] = make([]*common.ChartInstance, 0) - } - - tmap[deployment] = append(tmap[deployment], instance) - } - } - - return nil -} diff --git a/cmd/manager/repository/transient/transient_test.go b/cmd/manager/repository/transient/transient_test.go deleted file mode 100644 index d74fbfc36..000000000 --- a/cmd/manager/repository/transient/transient_test.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. -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. -*/ - -package transient - -import ( - "github.com/kubernetes/helm/cmd/manager/repository" - "testing" -) - -func TestRepositoryListEmpty(t *testing.T) { - repository.TestRepositoryListEmpty(t, NewRepository()) -} - -func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T) { - repository.TestRepositoryGetFailsWithNonExistentDeployment(t, NewRepository()) -} - -func TestRepositoryCreateDeploymentWorks(t *testing.T) { - repository.TestRepositoryCreateDeploymentWorks(t, NewRepository()) -} - -func TestRepositoryMultipleManifestsWorks(t *testing.T) { - repository.TestRepositoryMultipleManifestsWorks(t, NewRepository()) -} - -func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T) { - repository.TestRepositoryDeleteFailsWithNonExistentDeployment(t, NewRepository()) -} - -func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T) { - repository.TestRepositoryDeleteWorksWithNoLatestManifest(t, NewRepository()) -} - -func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T) { - repository.TestRepositoryDeleteDeploymentWorksNoForget(t, NewRepository()) -} - -func TestRepositoryDeleteDeploymentWorksForget(t *testing.T) { - repository.TestRepositoryDeleteDeploymentWorksForget(t, NewRepository()) -} - -func TestRepositoryChartInstances(t *testing.T) { - repository.TestRepositoryChartInstances(t, NewRepository()) -} diff --git a/cmd/manager/router/context.go b/cmd/manager/router/context.go deleted file mode 100644 index 6e878c762..000000000 --- a/cmd/manager/router/context.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package router - -import ( - "github.com/kubernetes/helm/cmd/manager/manager" - helmhttp "github.com/kubernetes/helm/pkg/httputil" - "github.com/kubernetes/helm/pkg/repo" -) - -// Config holds the global configuration parameters passed into the router. -// -// Config is used concurrently. Once a config is created, it should be treated -// as immutable. -type Config struct { - // Address is the host and port (:8080) - Address string - // MaxTemplateLength is the maximum length of a template. - MaxTemplateLength int64 - // ExpanderPort is the default expander's IP port - ExpanderPort string - // ExpanderURL is the default expander's URL - ExpanderURL string - // DeployerName is the deployer's DNS name - DeployerName string - // DeployerPort is the deployer's IP port - DeployerPort string - // DeployerURL is the deployer's URL - DeployerURL string - // CredentialFile is the file to the credentials. - CredentialFile string - // CredentialSecrets tells the service to use a secrets file instead. - CredentialSecrets bool - // MongoName is the DNS name of the mongo server. - MongoName string - // MongoPort is the port for the MongoDB protocol on the mongo server. - // It is a string for historical reasons. - MongoPort string - // MongoAddress is the name and port. - MongoAddress string -} - -// Context contains dependencies that are passed to each handler function. -// -// Context carries typed information, often scoped to interfaces, so that the -// caller's contract with the service is known at compile time. -// -// Members of the context must be concurrency safe. -type Context struct { - Config *Config - // Manager is a helm/manager/manager.Manager - Manager manager.Manager - Encoder helmhttp.Encoder - CredentialProvider repo.ICredentialProvider -} diff --git a/cmd/manager/router/router.go b/cmd/manager/router/router.go deleted file mode 100644 index 65d1e86fe..000000000 --- a/cmd/manager/router/router.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -/*Package router is an HTTP router. - -This router provides appropriate dependency injection/encapsulation for the -HTTP routing layer. This removes the requirement to set global variables for -resources like database handles. - -This library does not replace the default HTTP mux because there is no need. -Instead, it implements an HTTP handler. - -It then defines a handler function that is given a context as well as a -request and response. -*/ -package router - -import ( - "log" - "net/http" - "reflect" - - "github.com/Masterminds/httputil" - helmhttp "github.com/kubernetes/helm/pkg/httputil" -) - -// HandlerFunc responds to an individual HTTP request. -// -// Returned errors will be captured, logged, and returned as HTTP 500 errors. -type HandlerFunc func(w http.ResponseWriter, r *http.Request, c *Context) error - -// Handler implements an http.Handler. -// -// This is the top level route handler. -type Handler struct { - c *Context - resolver *httputil.Resolver - routes map[string]HandlerFunc - paths []string -} - -// NewHandler creates a new Handler. -// -// Routes cannot be modified after construction. The order that the route -// names are returned by Routes.Paths() determines the lookup order. -func NewHandler(c *Context) *Handler { - return &Handler{ - c: c, - resolver: httputil.NewResolver([]string{}), - routes: map[string]HandlerFunc{}, - paths: []string{}, - } -} - -// Add a route to a handler. -// -// The route name is "VERB /ENPOINT/PATH", e.g. "GET /foo". -func (h *Handler) Add(route string, fn HandlerFunc) { - log.Printf("Map %q to %s", route, reflect.ValueOf(fn).Type().Name()) - h.routes[route] = fn - h.paths = append(h.paths, route) - h.resolver = httputil.NewResolver(h.paths) -} - -// ServeHTTP serves an HTTP request. -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Printf(helmhttp.LogAccess, r.Method, r.URL) - route, err := h.resolver.Resolve(r) - if err != nil { - helmhttp.NotFound(w, r) - return - } - - fn, ok := h.routes[route] - if !ok { - // This is a 500 because the route was registered, but not here. - helmhttp.Fatal(w, r, "route %s missing", route) - } - - if err := fn(w, r, h.c); err != nil { - helmhttp.Fatal(w, r, err.Error()) - } -} diff --git a/cmd/manager/router/router_test.go b/cmd/manager/router/router_test.go deleted file mode 100644 index d34057919..000000000 --- a/cmd/manager/router/router_test.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package router - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" -) - -func TestHandler(t *testing.T) { - c := &Context{} - - h := NewHandler(c) - h.Add("GET /", func(w http.ResponseWriter, r *http.Request, c *Context) error { - fmt.Fprintln(w, "hello") - return nil - }) - h.Add("POST /", func(w http.ResponseWriter, r *http.Request, c *Context) error { - fmt.Fprintln(w, "goodbye") - return nil - }) - - s := httptest.NewServer(h) - defer s.Close() - - res, err := http.Get(s.URL) - if err != nil { - t.Fatal(err) - } - data, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - - if "hello\n" != string(data) { - t.Errorf("Expected 'hello', got %q", data) - } -} diff --git a/cmd/manager/testutil.go b/cmd/manager/testutil.go deleted file mode 100644 index cdf10bdc1..000000000 --- a/cmd/manager/testutil.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "errors" - "fmt" - "net/http/httptest" - "regexp" - - "github.com/kubernetes/helm/cmd/manager/router" - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/httputil" - "github.com/kubernetes/helm/pkg/repo" -) - -// httpHarness is a simple test server fixture. -// Simple fixture for standing up a test server with a single route. -// -// You must Close() the returned server. -func httpHarness(c *router.Context, route string, fn router.HandlerFunc) *httptest.Server { - h := router.NewHandler(c) - h.Add(route, fn) - return httptest.NewServer(h) -} - -// stubContext creates a stub of a Context object. -// -// This creates a stub context with the following properties: -// - Config is initialized to empty values -// - Encoder is initialized to httputil.DefaultEncoder -// - CredentialProvider is initialized to repo.InmemCredentialProvider -// - Manager is initialized to mockManager. -func stubContext() *router.Context { - return &router.Context{ - Config: &router.Config{}, - Manager: newMockManager(), - CredentialProvider: repo.NewInmemCredentialProvider(), - Encoder: httputil.DefaultEncoder, - } -} - -func newMockManager() *mockManager { - return &mockManager{ - deployments: []*common.Deployment{}, - } -} - -type mockManager struct { - deployments []*common.Deployment -} - -func (m *mockManager) ListDeployments() ([]common.Deployment, error) { - d := make([]common.Deployment, len(m.deployments)) - for i, dd := range m.deployments { - d[i] = *dd - } - return d, nil -} - -func (m *mockManager) GetDeployment(name string) (*common.Deployment, error) { - - for _, d := range m.deployments { - if d.Name == name { - return d, nil - } - } - - return nil, errors.New("mock error: No such deployment") -} - -func (m *mockManager) CreateDeployment(depReq *common.DeploymentRequest) (*common.Deployment, error) { - return &common.Deployment{}, nil -} - -func (m *mockManager) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { - for _, d := range m.deployments { - if d.Name == name { - return d, nil - } - } - fmt.Printf("Could not find %s", name) - return nil, errors.New("Deployment not found") -} - -func (m *mockManager) PutDeployment(name string, depReq *common.DeploymentRequest) (*common.Deployment, error) { - for _, d := range m.deployments { - if d.Name == name { - d.State.Status = common.ModifiedStatus - return d, nil - } - } - return nil, errors.New("Deployment not found") -} - -func (m *mockManager) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { - return map[string]*common.Manifest{}, nil -} - -func (m *mockManager) GetManifest(deploymentName string, manifest string) (*common.Manifest, error) { - return &common.Manifest{}, nil -} - -func (m *mockManager) Expand(depReq *common.DeploymentRequest) (*common.Manifest, error) { - return &common.Manifest{}, nil -} - -func (m *mockManager) ListCharts() ([]string, error) { - return []string{}, nil -} - -func (m *mockManager) ListChartInstances(chartName string) ([]*common.ChartInstance, error) { - return []*common.ChartInstance{}, nil -} - -func (m *mockManager) GetRepoForChart(chartName string) (string, error) { - return "", nil -} - -func (m *mockManager) GetMetadataForChart(chartName string) (*chart.Chartfile, error) { - return &chart.Chartfile{}, nil -} - -func (m *mockManager) GetChart(chartName string) (*chart.Chart, error) { - return &chart.Chart{}, nil -} - -func (m *mockManager) ListRepoCharts(repoName string, regex *regexp.Regexp) ([]string, error) { - return []string{}, nil -} - -func (m *mockManager) GetChartForRepo(repoName, chartName string) (*chart.Chart, error) { - return &chart.Chart{}, nil -} - -func (m *mockManager) CreateCredential(name string, c *repo.Credential) error { - return nil -} -func (m *mockManager) GetCredential(name string) (*repo.Credential, error) { - return &repo.Credential{}, nil -} - -func (m *mockManager) ListRepos() (map[string]string, error) { - return map[string]string{}, nil -} - -func (m *mockManager) AddRepo(addition repo.IRepo) error { - return nil -} - -func (m *mockManager) RemoveRepo(name string) error { - return nil -} - -func (m *mockManager) GetRepo(name string) (repo.IRepo, error) { - return &repo.Repo{}, nil -} diff --git a/cmd/resourcifier/configurations.go b/cmd/resourcifier/configurations.go deleted file mode 100644 index c1b457ab8..000000000 --- a/cmd/resourcifier/configurations.go +++ /dev/null @@ -1,255 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "github.com/kubernetes/helm/cmd/resourcifier/configurator" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/util" - - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - - "github.com/ghodss/yaml" - "github.com/gorilla/mux" -) - -var configurations = []Route{ - {"ListConfigurations", "/configurations/{type}", "GET", listConfigurationsHandlerFunc, ""}, - {"GetConfiguration", "/configurations/{type}/{name}", "GET", getConfigurationHandlerFunc, ""}, - {"CreateConfiguration", "/configurations", "POST", createConfigurationHandlerFunc, "JSON"}, - {"DeleteConfiguration", "/configurations", "DELETE", deleteConfigurationHandlerFunc, "JSON"}, - {"PutConfiguration", "/configurations", "PUT", putConfigurationHandlerFunc, "JSON"}, -} - -var ( - maxLength = flag.Int64("maxLength", 1024*8, "The maximum length (KB) of a configuration.") - kubePath = flag.String("kubectl", "./kubectl", "The path to the kubectl binary.") - kubeService = flag.String("service", "", "The DNS name of the kubernetes service.") - kubeServer = flag.String("server", "", "The IP address and optional port of the kubernetes master.") - kubeInsecure = flag.Bool("insecure-skip-tls-verify", false, "Do not check the server's certificate for validity.") - kubeConfig = flag.String("config", "", "Path to a kubeconfig file.") - kubeCertAuth = flag.String("certificate-authority", "", "Path to a file for the certificate authority.") - kubeClientCert = flag.String("client-certificate", "", "Path to a client certificate file.") - kubeClientKey = flag.String("client-key", "", "Path to a client key file.") - kubeToken = flag.String("token", "", "A service account token.") - kubeUsername = flag.String("username", "", "The username to use for basic auth.") - kubePassword = flag.String("password", "", "The password to use for basic auth.") -) - -var backend *configurator.Configurator - -func init() { - if !flag.Parsed() { - flag.Parse() - } - - routes = append(routes, configurations...) - backend = getConfigurator() -} - -func listConfigurationsHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "resourcifier: list configurations" - util.LogHandlerEntry(handler, r) - rtype, err := getPathVariable(w, r, "type", handler) - if err != nil { - return - } - - c := &common.Configuration{ - Resources: []*common.Resource{ - {Type: rtype}, - }, - } - - output, err := backend.Configure(c, configurator.GetOperation) - if err != nil { - util.LogAndReturnError(handler, http.StatusBadRequest, err, w) - return - } - - util.LogHandlerExit(handler, http.StatusOK, output, w) - util.WriteYAML(handler, w, []byte(output), http.StatusOK) -} - -func getConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "resourcifier: get configuration" - util.LogHandlerEntry(handler, r) - rtype, err := getPathVariable(w, r, "type", handler) - if err != nil { - return - } - - rname, err := getPathVariable(w, r, "name", handler) - if err != nil { - return - } - - c := &common.Configuration{ - Resources: []*common.Resource{ - {Name: rname, Type: rtype}, - }, - } - - output, err := backend.Configure(c, configurator.GetOperation) - if err != nil { - util.LogAndReturnError(handler, http.StatusBadRequest, err, w) - return - } - - util.LogHandlerExit(handler, http.StatusOK, output, w) - util.WriteYAML(handler, w, []byte(output), http.StatusOK) -} - -func createConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "resourcifier: create configuration" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - c := getConfiguration(w, r, handler) - if c != nil { - _, err := backend.Configure(c, configurator.CreateOperation) - if err != nil { - util.LogAndReturnError(handler, http.StatusBadRequest, err, w) - return - } - - util.LogHandlerExitWithYAML(handler, w, c, http.StatusCreated) - return - } - - util.LogHandlerExit(handler, http.StatusOK, "OK", w) -} - -func deleteConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "resourcifier: delete configuration" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - c := getConfiguration(w, r, handler) - if c != nil { - if _, err := backend.Configure(c, configurator.DeleteOperation); err != nil { - e := errors.New("cannot delete configuration: " + err.Error() + "\n") - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return - } - - w.WriteHeader(http.StatusNoContent) - util.LogHandlerExit(handler, http.StatusNoContent, "No Content", w) - return - } - - util.LogHandlerExit(handler, http.StatusOK, "OK", w) -} - -func putConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "resourcifier: update configuration" - util.LogHandlerEntry(handler, r) - defer r.Body.Close() - c := getConfiguration(w, r, handler) - if c != nil { - if _, err := backend.Configure(c, configurator.ReplaceOperation); err != nil { - e := errors.New("cannot replace configuration: " + err.Error() + "\n") - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return - } - - util.LogHandlerExitWithYAML(handler, w, c, http.StatusCreated) - return - } - - util.LogHandlerExit(handler, http.StatusOK, "OK", w) -} - -func getConfigurator() *configurator.Configurator { - kubernetesConfig := &util.KubernetesConfig{ - KubePath: *kubePath, - KubeService: *kubeService, - KubeServer: *kubeServer, - KubeInsecure: *kubeInsecure, - KubeConfig: *kubeConfig, - KubeCertAuth: *kubeCertAuth, - KubeClientCert: *kubeClientCert, - KubeClientKey: *kubeClientKey, - KubeToken: *kubeToken, - KubeUsername: *kubeUsername, - KubePassword: *kubePassword, - } - return configurator.NewConfigurator(util.NewKubernetesKubectl(kubernetesConfig)) -} - -func getPathVariable(w http.ResponseWriter, r *http.Request, variable, handler string) (string, error) { - vars := mux.Vars(r) - escaped, ok := vars[variable] - if !ok { - e := fmt.Errorf("%s name not found in URL", variable) - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return "", e - } - - unescaped, err := url.QueryUnescape(escaped) - if err != nil { - e := fmt.Errorf("cannot decode name (%v)", variable) - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return "", e - } - - return unescaped, nil -} - -func getConfiguration(w http.ResponseWriter, r *http.Request, handler string) *common.Configuration { - b := io.LimitReader(r.Body, *maxLength*1024) - y, err := ioutil.ReadAll(b) - if err != nil { - util.LogAndReturnError(handler, http.StatusBadRequest, err, w) - return nil - } - - // Reject the input if it exceeded the length limit, - // since we may not have read all of it into the buffer. - if _, err = b.Read(make([]byte, 0, 1)); err != io.EOF { - e := fmt.Errorf("configuration exceeds maximum length of %d KB", *maxLength) - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return nil - } - - j, err := yaml.YAMLToJSON(y) - if err != nil { - e := errors.New(err.Error() + "\n" + string(y)) - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return nil - } - - c := &common.Configuration{} - if err := json.Unmarshal(j, c); err != nil { - e := errors.New(err.Error() + "\n" + string(j)) - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return nil - } - - if len(c.Resources) < 1 { - e := fmt.Errorf("configuration is empty") - util.LogAndReturnError(handler, http.StatusBadRequest, e, w) - return nil - } - - return c -} diff --git a/cmd/resourcifier/configurator/configurator.go b/cmd/resourcifier/configurator/configurator.go deleted file mode 100644 index 49255be94..000000000 --- a/cmd/resourcifier/configurator/configurator.go +++ /dev/null @@ -1,262 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package configurator - -import ( - "fmt" - "log" - "regexp" - "strings" - - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/util" -) - -// Configurator configures a Kubernetes cluster using kubectl. -type Configurator struct { - k util.Kubernetes -} - -// NewConfigurator creates a new Configurator. -func NewConfigurator(kubernetes util.Kubernetes) *Configurator { - return &Configurator{kubernetes} -} - -// operation is an enumeration type for kubectl operations. -type operation string - -// These constants implement the operation enumeration type. -const ( - CreateOperation operation = "create" - DeleteOperation operation = "delete" - GetOperation operation = "get" - ReplaceOperation operation = "replace" -) - -// TODO(jackgr): Configure resources without dependencies in parallel. - -// Error is an error type that captures errors from the multiple calls to kubectl -// made for a single configuration. -type Error struct { - errors []error -} - -// Error returns the string value of an Error. -func (e *Error) Error() string { - errs := []string{} - for _, err := range e.errors { - errs = append(errs, err.Error()) - } - - return strings.Join(errs, "\n") -} - -func (e *Error) appendError(err error) error { - e.errors = append(e.errors, err) - return err -} - -// DependencyMap maps a resource name to a set of dependencies. -type DependencyMap map[string]map[string]bool - -var refRe = regexp.MustCompile("\\$\\(ref\\.([^\\.]+)\\.([^\\)]+)\\)") - -// Configure passes each resource in the configuration to kubectl and performs the appropriate -// action on it (create/delete/replace) and updates the State of the resource with the resulting -// status. In case of errors with a resource, Resource.State.Errors is set. -// and then updates the deployment with the completion status and completion time. -func (a *Configurator) Configure(c *common.Configuration, o operation) (string, error) { - errors := &Error{} - var output []string - - deps, err := getDependencies(c, o) - if err != nil { - e := fmt.Errorf("Error generating dependencies: %s", err.Error()) - return "", e - } - - for { - resources := getUnprocessedResources(c) - - // No more resources to process. - if len(resources) == 0 { - break - } - - for _, r := range resources { - // Resource still has dependencies. - if len(deps[r.Name]) != 0 { - continue - } - - out, err := a.configureResource(r, o) - if err != nil { - log.Println(errors.appendError(err)) - abortDependants(c, deps, r.Name) - - // Resource states have changed, need to recalculate unprocessed - // resources. - break - } - - output = append(output, out) - removeDependencies(deps, r.Name) - } - } - - return strings.Join(output, "\n"), nil -} - -func marshalResource(resource *common.Resource) (string, error) { - if len(resource.Properties) > 0 { - y, err := yaml.Marshal(resource.Properties) - if err != nil { - return "", fmt.Errorf("yaml marshal failed for resource: %v: %v", resource.Name, err) - } - return string(y), nil - } - return "", nil -} - -func (a *Configurator) configureResource(resource *common.Resource, o operation) (string, error) { - ret := "" - var err error - - switch o { - case CreateOperation: - obj, err := marshalResource(resource) - if err != nil { - resource.State = failState(err) - return "", err - } - ret, err = a.k.Create(obj) - if err != nil { - resource.State = failState(err) - } else { - resource.State = &common.ResourceState{Status: common.Created} - } - return ret, nil - case ReplaceOperation: - obj, err := marshalResource(resource) - if err != nil { - resource.State = failState(err) - return "", err - } - ret, err = a.k.Replace(obj) - if err != nil { - resource.State = failState(err) - } else { - resource.State = &common.ResourceState{Status: common.Created} - } - return ret, nil - case GetOperation: - return a.k.Get(resource.Name, resource.Type) - case DeleteOperation: - obj, err := marshalResource(resource) - if err != nil { - resource.State = failState(err) - return "", err - } - ret, err = a.k.Delete(obj) - // Treat deleting a non-existent resource as success. - if err != nil { - if strings.HasSuffix(strings.TrimSpace(ret), "not found") { - resource.State = &common.ResourceState{Status: common.Created} - return ret, nil - } - resource.State = failState(err) - } - return ret, err - default: - return "", fmt.Errorf("invalid operation %s for resource: %v: %v", o, resource.Name, err) - } -} - -func failState(e error) *common.ResourceState { - return &common.ResourceState{ - Status: common.Failed, - Errors: []string{e.Error()}, - } -} - -func getUnprocessedResources(c *common.Configuration) []*common.Resource { - var resources []*common.Resource - for _, r := range c.Resources { - if r.State == nil { - resources = append(resources, r) - } - } - - return resources -} - -// getDependencies iterates over resources and returns a map of resource name to -// the set of dependencies that resource has. -// -// Dependencies are reversed for delete operation. -func getDependencies(c *common.Configuration, o operation) (DependencyMap, error) { - deps := DependencyMap{} - - // Prepopulate map. This will be used later to validate referenced resources - // actually exist. - for _, r := range c.Resources { - deps[r.Name] = make(map[string]bool) - } - - for _, r := range c.Resources { - props, err := yaml.Marshal(r.Properties) - if err != nil { - return nil, fmt.Errorf("Failed to deserialize resource properties for resource %s: %v", r.Name, r.Properties) - } - - refs := refRe.FindAllStringSubmatch(string(props), -1) - for _, ref := range refs { - // Validate referenced resource exists in config. - if _, ok := deps[ref[1]]; !ok { - return nil, fmt.Errorf("Invalid resource name in reference: %s", ref[1]) - } - - // Delete dependencies should be reverse of create. - if o == DeleteOperation { - deps[ref[1]][r.Name] = true - } else { - deps[r.Name][ref[1]] = true - } - } - } - - return deps, nil -} - -// updateDependants removes the dependency dep from the set of dependencies for -// all resource. -func removeDependencies(deps DependencyMap, dep string) { - for _, d := range deps { - delete(d, dep) - } -} - -// abortDependants changes the state of all of the dependants of a resource to -// Aborted. -func abortDependants(c *common.Configuration, deps DependencyMap, dep string) { - for _, r := range c.Resources { - if _, ok := deps[r.Name][dep]; ok { - r.State = &common.ResourceState{Status: common.Aborted} - } - } -} diff --git a/cmd/resourcifier/main.go b/cmd/resourcifier/main.go deleted file mode 100644 index bc8ae084a..000000000 --- a/cmd/resourcifier/main.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package main - -import ( - "flag" - "fmt" - "log" - "net/http" - "os" - - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/kubernetes/helm/pkg/util" - "github.com/kubernetes/helm/pkg/version" -) - -// Route defines a routing table entry to be registered with gorilla/mux. -type Route struct { - Name string - Path string - Methods string - HandlerFunc http.HandlerFunc - Type string -} - -var routes = []Route{ - {"HealthCheck", "/healthz", "GET", healthCheckHandlerFunc, ""}, -} - -// port to listen on -var port = flag.Int("port", 8080, "The port to listen on") - -func main() { - if !flag.Parsed() { - flag.Parse() - } - - router := mux.NewRouter() - router.StrictSlash(true) - for _, route := range routes { - handler := http.Handler(http.HandlerFunc(route.HandlerFunc)) - switch route.Type { - case "JSON": - handler = handlers.ContentTypeHandler(handler, "application/json") - case "": - break - default: - log.Fatalf("invalid route type: %v", route) - } - - r := router.NewRoute() - r.Name(route.Name). - Path(route.Path). - Methods(route.Methods). - Handler(handler) - } - - address := fmt.Sprintf(":%d", *port) - handler := handlers.CombinedLoggingHandler(os.Stderr, router) - log.Printf("Version: %s", version.Version) - log.Printf("Listening on port %d...", *port) - log.Fatal(http.ListenAndServe(address, handler)) -} - -func healthCheckHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "manager: get health" - util.LogHandlerEntry(handler, r) - util.LogHandlerExitWithText(handler, w, "OK", http.StatusOK) -} diff --git a/docs/design/architecture.dia b/docs/design/architecture.dia deleted file mode 100644 index e2256d456a7bc3ea0afe714abb80a496d94fa400..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1785 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 b8WOIL%P$ta?bW_{^tSgOG?jK4om>C_8(Vc? diff --git a/docs/design/architecture.png b/docs/design/architecture.png deleted file mode 100644 index 9b7966c9208532f5810fe34d5e30abf6a39a1d5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13472 zcmd6O2T+vjmTjXbARq`L2oe+pBqKQoB@2io$pT7Fk~4@XIVedaDzr!zN!lb2NS2&I zKyuExNwZt;IdkXSc~dnruU@_1Ds7wQ58t=<+H0-7JNTZmEa5rIa|i^2P+soNeFWlE z3j%>9i+c**8CZ+^1pk~hR+PPiIKli$tIUi>Am|YCcO=!_l9op-9$eI_YFM|7YHiH- zr)$lgU>Xe)2o@l{di_f$qm)0_OM!ZkA3-EHSY6*SolSpIdxOH1LDhue&2&PTkmcPg z+u44q>21<xHP;qC+D$|}eAe3KZK!@fo{mmYvd~MM!%I0;$a!Hb-0A2;cE0BPwg;BC zoTX)Ub#--@1_FT=K8?hO`}6KVAn_&Q*ETm5<>ggT=MWOU*J+eyDb6AgQM7oc5s1df z04xMzO11_*yg_dSfA7EKL?HYQ<tPvc37_i(@JQk$|Fe(Zx|gM{5RX92Y5#e$;@at! zc(d=XjjXJ!jE!$SnfO-ex$9!e7vY7Fpp~hS&^<aBJK5RX+}z$iniW6M*4Ex{5I<gD zEU4B*F(D*~q~Rl#yQ7{LE?hV{8av6;%u%{~H^prtI55(PN09ZZsK}FvfU>f(goK39 z(3S{l;S6c!SOv=T?s?4ugXM(<OA`}PO3Lg`^9*?sQqtt4Bm#oH)pLl-vvz~#Ii!rr z0RaI{)+2(BGvDg!G}P6-PL6hG+mcDKB@6mPPeKSKiA${qU%h$rCMhW?F;Q1bi=390 zBhP1Js%dk!ebEV}ERKhe;J<2=mS;OwwfysEN1-zg0mVH{O_#}f!V(=enp(F3##FD3 zO!1?p-rnA!A&>6&O1=lfV;R!4K6^%i{Dg>m*m$RH9$Q)IYHIcsIul(Hh)PNl@j2Xk zNiRzy=Jk1y>-qENDJdxv?xdcc2NM&<7B6-3Rt8+K5DJ*fo;r2v7~Nl8DpudqQ;b6C zSjWf5D;|xGjvnl;R=aPS8XFs%n|~M>oO%bhlbo7baH4(t_Os$3m1KlO!R>!6uDrZ_ zaB%RA8#i?Fc4$xb8d^B2e?0mgfJe$_GnCP3zOe}po64~r7!aU`a>~ljS0=`u_iz2< z8IGBmnW9NeZS8DpVtjJ4zOHWK%a>eMeYyF*$_T%FDL8#N1=lq}!Tp{rH3I{KOP4N% zUSjhQz_uPiZLf{jKEol}+1>R$-k(Jtr=_L+{P|E(vBh=FdlzotqOggHiMO{ma*`DG z{^`@FfBf+W{C<&H`^uUI65*F3aYMf;vg#@?Z|~UHSbICijT_SQItawk%g>Ax|8Jrl za>>rG>kOpMeKobCqodgTQ_DYRXX9VJdgnwIMcVKys{~$PrL|AAPZ&Hst?W(q`@KJt z<A0WETe!@z*|mzof`T1naHfvX3)CGZkiu!?-HY3heLVl3;F6i$c?Ok35P9|W3_LvQ z>7BU##56R=gJ!AnCdY@f;(>vI`2K14v-qqB9>Qi?S(Tf2z8f0SdGfV4N6XM>|FN#F zF6_zApFefWY=e)OV=Wi^^4QtgO-)Us8I+p8f4_JDiA~O~XAb|^*;PN8lquZI%1l9{ z7YmT#m&nKj1q6PSyQ&ZenDR3x-%!m`hd@(SRz7?7ESu7KCq}>-T-{P@c_k$!W#zWk z*1VjY(8x$~?D1M6@gH9tXIDnb&q(eq8lIr6`tvg~u0J^}G-+TrsCb={;xpYG>%7<( zoXl<3ruW(22!dEu^<{ecWEWBqxgSX*3R&v`cblb_$^PJ@#FAdQ%Q8GvHymAE=V`@Q zUFz%V@^nfRGP{F`wDNSwE?>5=uo&!;p%QlAnA+c*t#eLHzA~rjvBh@t=1qUPz5V^0 z`nYKsJIB}DJUk4lytFa7CM+zhXrraoKQQnB{@&PdC{!a3$jxOfOioCcUs*X`3)o#5 zp@KZ-IUXvpGS*G+e4UUGY~$+cYLOBaMlet4KJhKs(0e=9prG1gdjXPymr{v^Dz=XV z`)yj91alg)$&+rh1WmJU*33nKAU=nWFDfdk0T_q;R942p&VK1&d1-0pdqR=V5&Ahk z`Ox5?{N%`wAKzY-mwQ@RWDO`QD&phfek?46wU3L7>(AGZt{)yA4h?^L8mF(PCuq5= z>%IjI5fPEaM>mS}w{MN=LdZhNIcNmzAF@!{+|AQeT#jSFK@mdf!2X3G97kvRIy&ff zPWC1veUCR&cbA9bU%!4G9ZetJ-qI3H84(d-?Ae#A?Ks=k+SHU)P*ClCP|72(c!io8 z>F(g@h&=xKRl>w%5~&!UBI@aKe7K)yS6o=1s%K;2kTx8}iIBi0B1+}AGjMTnv9Ym{ zk*UwsF2a`VFz0cc`M$r9Q()jRkBk&Q-iy^TczIs@j;w4vJXc(tjI3;Xim2{Vk2w$E zM5O4>10y4&2M-45`O!mG1@fDBq^0E{fSK9ZZ71u)TjDwR`S{|{i6iEojK3c*ASfv4 z>({S<LiqThgGFWlcews*eY$qickYO6_o(^P-AxtqZc7rxCuhIdfP;<g=IQC_?*65` zoI|(t?$Y5_w+cX3Yilb&LFV0rjmB^YurFV}xWnP;DZcx|F`WD|C1nH!m+{JvFX6&) zX&hMDK!x@Z9dukzkGij~ueiAQx)fEK??RU$c5HUbnBeKtr*BJ1eM{?}UC-~`4HECh zQi#Vw?D+~M+crc{pE+};pbDMcRf0&CszLbCeCz1wsH=Mn>o5^s<Tn}$m43M!nJFjW z7%uiK3@ZD7qwEXYVo8`~^)5gmVbPb4gu<{+>$NQBdmAp)mi^(whwSY8s1Ta){!H3y zMhHKVkdTnGgp?UhWTI`bJ9De61x{r55D+#~MvosqwzbWNpPVm{uda(t`Cs!4M8^fl zfO(RR^uMS}2emqL?+ZezrKKfn8yj*q9d%t@mK`Y3kn`mz)X=~{z>61_Jd_q*#>VfW zqobcna@+MN#X)4+*eusP$A?f`US8(6{lP@-Z0@ip=`qkH!l<0IwzI>>&F%Q)$zY*L zU~z~7azgH`4rZ-w9hL?PAyM9EW*UBRcszn~HlB@R(}hX^CwFvoTwGX~n4CneGpeMB z_#Rg%XIi<G)+_41d9*xKqE%p^q@tpts3`2Uw?@xmZISYSBp_**|BgSl$I_hq{N&hg zKYjWX%jHk^=<(y=<?r9`0;Ci!&(FK>Z#*v4H!xUPT4D|7U}7?V{OhvcMn?39%w=R= zbTE<fB;;+o1EG2K>J@C-8~q$-06vKB*PL&WSQS>=q7&^gCQw+Ee*6=Tu(PsmZEamt z;U5?tCMPFXob0tM>CrsO9YnGz-Ph4!w&|&@oqS1F%F_<O@+vyIA(B?yVXCq83O*J# zpuAIJMTJP#_}m;*X|JWTk5AQBgCP(Ro~kD)QD=F|i;?med<dVJnNgU8Vvv)QQ|IFD zzE3~9zOyr0?R(Oi$WKa2di(ZmT6TmR1>R|iR1GbyOeef*VrpuA9n3*VO)cuVV_nEC zBH}e#;XXJxSW;4Q`SRr=)7FKr-`SKlFepG39uo4ZUgb5X5%4g|a0YR4UjU%UB*yF4 zr=V_3EZ)?27jj$s2~l)jJ=Dh-Q{E-aMk+n|SIzeK_aT#kq59MPKshVUcllhUqx(8M zT%MnA3wYNO$3`klf9)Cp0fGOsXTWG>Bd+WpAFR4QoQY!y4h;<r3E9}#xOM9m`e@bH z_i*n?p)XXY&6!rHtjWpABd=~Ko~IERK#NF9N&?pb)|W0DdA!lWQP&2=#n5mpB;=eZ zx#v4U=3}!huB@!A<Ktr=AD@Zmd=g`hKu96(x#WYGnVH{E3qEd+>FMdQWooLad6%7? z{pr*Fim4)P=GcwZvXCD8@tQNVw5fQc3_<Z;Vqva%RkDW(4g{wqI2@s*uC1+=hD}Lf zNl5AiVx`3=@Y{`Uwu_IUU&}``IID0WFu~u^@<BbjUHrsruH!8mAtx7Ca8S@(+p8g| zb+zlF8E3a>K}1xxM_s)Q5e6GWLnCHA@Np#<=W8wS6^|Pb`YbFtfd`OsRAX-Cz7wY| zZee47CCs4UWG+3DK_x{`Rn^GceBjai#&k<wULM&^0tu7r1Y4903=GU7%~MU$vB#3m zc{y4I$|)l5<2BD=D_L2sfw_mrqrQA8SvO(u4hwm9Y0e#UL7Zge_^7Bgs8^|?o_(F2 z>riEfOKlEDR(*EssqExT`0Pfl0TuMZ5yv2rD#J8$mTNe$ms$bq)3=B+@yW=@j%Iz2 z&8)4hAvPa8cwlP!e(Py3#H6r*0Mv-Yu|ozXCb`aA(;5Ftr6h?{JQlJFs*iOj)zsCe ze*UDR`ASS=EFW5GJF)<L-qe)OqALRuxKJ&pS4LWT5UTU=u&lJSMTw4uZfgXN(6a!n zw9Mfu@ACNg{`JX*K!VGk%-U0IhD&*?>bvXfXQEZaxHvfnD%>4ug|+Sf4om)YAZ`A^ zb53ji;yGD%Tha?%DA`CFc$`WgQ9#ehSs&23<ZBl(u&}VO{W?TNy#cH8B5{PIJH=6S z;_Id55N`_P*TkXBJvE_VBaJlLL!+7XgOU^!6f#W6AZq&y4DBqY_{Y4u_l)>Yt_AVq z7v$!~JW-Y;2DJL-0_#>@UOtc_@MJ>E&>PvmSYS{o<h5rXE|YE-!<pe+pkK~V%1apz zYHq;A(J^}Fdt+nNjI~7t>$k+`khOqM-h28vV<f)suhb$sZrhAidBgKUrWKZyK$h-~ z9q*KBu6!b+pcqIlaVFyD=Pw*zSup^7Dy^%fAS5JY(ae2N;kN$u>FGrM{?SnbYl15R zAK$;1v5t?8m3Q=TbCYu{<)N7)AUW$tGuxM^3*<6M+}B51TKeI`ho+`Lw76s06|evZ z?$>s9s#@cmJxfVN#i5VdS{f|sA}=Ug-->$n>=}=lvT_6u<1d2l5qEU38+n=Abak{6 z@C|4vdM42>L%iT1ac*v|>(>D<fN_K-|1&=!SD`-V&-ojSWR)pMNJuVU*1nH9;KgMU zlCI~y>0=<^=WYU>*lcDgz{bHD`}|~bJ~OqgvC-{lyC3dU9t#T#)F6njm6a8MsAx1S zf@~NCm+OjN79$^DUw{AZOW9j-9EM^ZTjsQwBbCdf9zw=hV;-uv1kw6(*;oYJJ&=lL zX9<T&ZH6HR0kGiYMo|B%fOjSfyNaRb((8Bk_YODPwes}@AAUt-F)}dB0DniPiYvZ5 zndVQDCxU2;e)X!k1#`fQyGlyBv_wwzezHVZe);Me8YKWt994T#!~vO*GezxU(fKlW zNdHcCA|+}NX&EswF+g_w{ZEmSMuUhC3c@{oIwG&M@h=8cp)j`(*&p+^(;P&w*!~o0 zrNQdtWDy7n!=nW)1@hi!Jf7M9o8)uSa(^H$+As8EJ(;YBh#F~ZG(SRbLyFxnKzI{? zWOHK4(=0cx3sFks<K^KAqZRi78VySd=Kv&RK5v>fi`fS;&_S<-U2Qcr<KDg<4<Tcj zn3xC)3yWbtClOBvKR*uzX?=Y?k>4)ApkQ}v3nOqyRYcx8sUlFs{qO+?V6~Iqf=Nk9 zNSuT)1>tzV-Is*JPYe`Y8a`b5`t|D~`%}^L^YipFVFJzzJ?QO4B$cS7!^Af{J3BjW zZf<h&cxqvnpk$?$&cVI$zwx2rr6iD!gGLZ#TwI+^P4b70Ihr|85<yD95g8KFWmz)u z5sgOg?YaGugNuN1EyppPv%s^QVgYX<zpBH-!=d=wRUhqCA8&IQ8X6))7v4f<kdl*Y zsj99Y?T)1iyRN*PjD$N`pQr=cz@n9(t6P>|S&M<|<3smkWk%eZH@sE=3cusOtGgSP zlqEu5P7WSR-NC^DmKPLVX6iLGZC#ZpjXpi-dtz+h$4LB_se3n6bn-Okr93`Be&YK7 zTwKg?y6RDBFxJuW(dWnm)<KdO2+lEpc4i{}+>9YMFfvJEW)>D<k!R7-YLG*!s;ZN1 z40fGexBnTNJc}Ux?-B<<Tv@zMOibiqrKi75IWs=~^5x6UakK04ii)JzMl}I=Y=m>Q zt2#$<d)(7tHY7~X%qS<^y1~a+`u;ssB35qhP0-jHs1GSC05wfbO_RWf1RbWvYXfNo z4N!%sf2cDcP}@fSbRT*xv-CAJ83;Gm)}mg-#>BX}xj7x)e2Yo%sBnV<fVuLrvfcUa zOloTCljGfLyW!H-ni{Ejln0ib-74VAScN+`7Z(?3{3RY<-n{($OT*LC)8P%X)6?>V zva+&;Y)5AX(VXM)@lFB)0&=ym!Wl*QfZ;6U;$wK+vU}9rS2FK@ce-moJ?DvG| zgakp@4A@;>DJdy-eU!SE79+0zRx&*)!`*>mi)fkjruOz!abKUw_0G;t<gWu>fJb9% zF;q%`kB1i=7RCzd`Q}Y{L4b#M;b6>dG8}YPMMXuq>ncdLmQ9h)R1kfQ4-a;hO&TIZ zfiwR6<C*K~=+XXcs;;*7O?`Dcl8YBFUb$jzVX?nbzUs3-9dC?*W+GZK5x2F6zP=~3 zslHX~dwzZh$N>%x4p-OR$1QOsJT|&7*bJ+@w&ps;ejO(DVZg&k560YQsew=3RD1uq zyqsSw=Tvd51G#^i;?}s;`_ElP&I<bT?jRB44Slq|#NFqS@%e_;qpfFna{Be>2gy7d zg+;VHc5ds<2Omo#?|UDvH`v<R0x`kdy*GNREm;`qk;U(_4l*;eHs7EU4Loibd=(v? zvjEb-9(BBb8z~ce-@kwFYp^OAT3fRRvjsdpLMlGF%t~jWk%itIeSLi!Yip_7x1lKV z_)h9x+`0toL?!I9G+OBiKXHMYVmDUx_I4n%{zX5UxBQjySjkR6%iv~40A5P^4H98X zkB<%_uLwH8B_V`3zzw;=4PqMXg1DHNi(t|S31N0u=V-1o9f)O_?Z_K&n&}&$I-@>* z<fNycoTv*0F%-ox`025trKKfA4?Hv6oR?Qc@rQM`a~@TY@f91*EZnBwp#;@$&36}l z`efRclm+0{K?SMA;+>R5k6v4^S5CQc0sC!k?p^Cp+4K2O@I`sJxjo%tJ$F|KX~pz( zD)lk`#iyd8Iu`)awas-)6_s|(Ixd5|0-$7ZTwlgC=eHuF=&<%v5+wL~|0BSTk&ksd z6Rh&>e`vq*2`aii=L*=zz<2ii6((H&&yg2te|a6n#l;XMR8&;43Xd%;+?R)VEw)|4 zIapX6ot?>syIRj;OEUe|Id(b^?d%No^jO&z$jG}jG&CxY(Tm_hLQ(<m1^@;^hw75X znhW+(pipQ?PC)|69!x!Vkq7KgSooQ#sh<9ROy0hI`_^QV`pOk>QR_;bim)V!dwvU` z)V%dWrK*5;I12Le>l+*BiBJw2&!s}(b;$xxYU=8w=iec=?g7v7kxcId(kZetU;?TD zTn%a(T3`dDq-vU&;}R(;{WchR?TP#x!ouaHrF``CCLl=kH+ZJrcbd<%r_vG=-+S<2 zuSZP<nVA{*{Q35w9JYkyb%LMdVn<uupe_I%A&np*oZJq9af3}>4^}#u&`r^dRF^LY zAN|dOja#wpiS`Ds9MS|sObZLwG1~Y3eah5bEI-qioE?U9R3i#sVgo^Dm3!?w0M&>4 zzH=v_XKZx+{rmSozstcO+FtB`=I{T4h?aeaih|<KojZ@r%*5T+=)mG)V)`(+?}?kI z3D=)rT}_RTpPig^1ghWI*f_fFu7gk*Pd+ur3~&Qm0?Pv62h`zR*L3kP#5Kd+*HJMs zJP*J6!6kt8yL^Amr9gfrt)C*z2}>e%dSU|8iWnLi0^1o30v3H`;hw&}`i2I((TXos zRmbZMv|}Y!{SeBS>dgV32TI`)=E&&MC?1BgR{(nq(w~ZwGXFv0HFkDoR1h{|C?Wvs zCnFFn$T^SI%3T@}H%`l*4;Xp`RU32tGO$XBu_Zq{OgB^6RXkbw0S3>r8;Wtc4+?8Q z0$_IB_&Pyab@#jz6~C<>7z;{D;emk-04iW4<T|NVzS7B?p}9)rIc2lhq4ko}&xehl ze>a?eRKOcutbp`5ST5_&(`6jJPfdMKRrReCo&*sHHuTL(gOeu4)QZhE;n>(%z<odv zx0NrmG3?o8AQ=}DTzTdN2?z;e*|A2#1F*Jap&4LTwLjCDE_+527axE4qtP>f_b%ye zV~98~nYRRfJi(uHEwvk4nVxpunp4HW3A`P69)dUA4NJlWq89xUh)&gZpDrXe8MYB< zDX8a%>x5~cQJj9q#*iB0<Ky7BPymR_(t3ltcNfWWsv{nrscLm+c^H1`xwCWud%TX^ z5cGBqP)<y~O<w$b{#kqIq>;0;bM=0!T?DOozSC9j4(LXJAy#hIPD4eNI0D6GtUA>V zD~*cD9m%MAnj}`i85oLLs<#VBWx!`uRFW7-<Av>hL!xu%m^nCn$Dfnm;^r3QK9x4i zl)x!;xc=mw{FuqZwQk=!OsfXyp^C`*H|S0@5S|sH7Ze<9X;}>Ar~)r$3^*}dK9G>= zJ)j->f>>S%jids_JKw`e+U`u1-Su@r`=1EG^hzGeRw%Bi?lTEcyCLC#83DXH2>RLf zmEfgURY8>+1>XX?L|_Xj#VOdi1NYrt=*`d1=i}o8AfXJGq5SB)s0juPH+K;z^2es8 z3RYR_%%fZfuNki1CJw+b@~tjK8XB76GP{C;0<DZ}gUnB#9DaO$0&ma?*#Cr^RZbEZ zpO|>(q#6RoyT;pw&D~uswC_38oqnIV>sPOigB%3P0+-Cu%(Ju@_{-MH%gr72IamgA zcr)UPs0R=Y=f<e3u+0V^AN~QAW!s|?46@as^XAZV%FVTQQPtNU0dkV4pV4axmQJF+ zy1ss1uVtdX)5-A>3WYKy*U;1~T}+GuX9RF6H6|vbdw!@&vl@aF)FG|!;ktggOE?w3 z^1K6L%TS#9?A$x*ouCW!JF8>jE=vQ3)*G{4bi6W=G~PeIK2=SZ!SlBV>du(r_72SR zEA{~JP#`3UvvYEAh-g?>7EnS)pfj`|JfNVW0#+nmQ5<{ZWMh-(gm@nep8(T@r~CuF z1%R*UTRv9qS}-VT3tcGqe^JgBA$a*kA3xqhg`~^{()zp>asLEFA9$?K$-&qrFEJtp zT7?U%t79d2wco!lf%pNa3m2wUrZ|mr-nwF!my?tDq~7M$bsB@h&>+0@#Tl)Tr%xpn z`2+cG-J;|%zgHA`_wHRC^d}|QT?TQzrZmZCARyl8KQc1vTxwhGRuQ+E9;k=shhnn0 zxLBf-S6KK&vGxvVnxpYR+Ny(<^1i@rF5{ZD_4WO+lOsc<6;?+<FkZS<`zrxK!Re_f z9>EvuoFzKmQ;jc6Q`t0~TgWJYx&S}-`ivpZ$ivQNpCMR=@85F=rQxo5xVS7_R9Oxz zpbHepp+}584y7IHAUFZQ>RR&$OuT`QwWdT|=C$lOSgjTZk`-t0m4Sy+$=G;;Nc@ma zY{_o6hvsrMBF)purUvLA|Fvt^pzf$ySuJYhXx?C8;1g#3?b!i9fJz1Zs12rFzoG9) zb1}3C+uGV_L_M7P)sIM_Z2(~DG|zzY2V=eKhEqrFA5a0F04)KuGA-YE1MCP|$<$oN z0ec?}>+lhCf+#*C4D3;cn>NSXqzs=KaUHX<cmSmcR^Hy;-pA*-@g@D3@5xbLCJ3AD z?d`k|A0Ezj#`B0>=LRM<Ffeci|5EY#XK9l$p98kt*i(o)=jBX|@USqbv1)pHUbAh~ z^VwZAhhVQ?l*h~h$ltsqM+mqMJQsXK2&;_DOq?@kcrSNM8Qs169l$A=+&k<wXKr3z zf39|PVj>ka^~h7VtGH4+Izt)q(HVhVZ!c&#()#$kRzRY`wgNr`aRDXS6TLML)g<DI z;C;PCDAW@lHN}ia;(AMTY}D0bDlvyQyl~1YP(Ui&z3b%QaEp`EZ6Pa@lJ+|bGqZ!e z{b$!z0~`ZV#GDV{x9IcOSPd^PFIa<5k6Uz5Nk}#j=ej6&<lgu!*pf=gLe78qpJ8jY z|F0RPqLn;t;a}Z5yZ!~>Kuyif3W~oD6&1sBm%L&$a0w6#1>N%?i@}Gw;caZ*o2>!D zicLQUA0Zk;6COk>!^_XFg^G$G=2+X>+*CtFx#R$jc>B5WzR=9E@qP{LR3`l;jlxOg z8OhHs%Wnqy%z2;}*_zLeE2XWSz>|wC5fK&TFsx!N1&;wr#&8J=WSraNxk`i7moLE! zV$U<UcaM&Y8_GYlP816I4J$pI_twV4Q~PV7M$zN?7ZljBm>|-qf;_K97^P)KEmHOx z)xH2k;w_h}C<|F@A(B(%zho`)40E;L{kS?Z=pBH?J6z%ccL5h1*jZZxITfp^mD77) zS0t<xatjQEx=`}6twK=xPvt1)tYGE8RnH8|of}ym3em}HYj2P5v+37YID?7H#+DY+ zVNhq#PV0JCug%EG38CmV--QHUA{iuCIUg3H`I=E$L4ue!8pJ@k2g`(PZc$MYG-A1L z-7;-`)0^K5t)px7^on55DJZ}T`0pF)_xH141Ndxzd<I@->x7l&3UdSn31IpFr2(lj zgwzE)7Z+^o^XE%HetZq&N%6wl8^FM6MLmimF-QArE)zJ14yA>;xq$JvAoxWW)FZ^4 zroN{*LD~V~6#-5F6(J#X$J^Udj;Rs0J4T8}Y@!#%jj1nO=q&90?E_)VVMBf50F03g z#`Ed3YdPoA;>T_9y*t2)k7buU`*-I@q8!KpP4izCPpc0WA!5rO2veZtYJ)B!O?KRl z9`S2@CGDd{_xn7<YJ+)ZgoNpJ0tq8pueCpLXuar0R!9-(v-!0MZ0Mo>lqQ1N3aGnd zUYkmucEiu(KK$A7qE%N{?@6$`aR<}!irEE!?TU!oy|{q4{fn^EK#_&SHC8piM$phe zI+1x3Ytg3b!>$@aab0f{UIivQJU9sY^RShF>|!;}okK0J(_ot@#<A{0bNr0tYhEi& zDJg&GhYQGvq^s)aWOvWQ@}sN=KY_Oe9*m!#-|}!75sk<LJyB+6OQ=0EGBQAO;S3it zd*_`npbtIA)sgaDkPgS=M80lIg$>`b!9K6>*tXP8`3X>=r>B?VgPtz~?0QaaV1CHP zctq%H-x5?SHTT(ekG<a@0Q`f5p}pnhJSrFhfIy;!rv$NJc(QG1Y!+?!6f=*JV3u+d zy8i6!Fs=dnAO8XxkFjxaAw&(~RPjkkdY}YVRSRKs0Gwa7CD7cMfsfzWpX#|HXt4n{ z5;RPwrl&EY)V2CR|L)ze;yP$8mTlQ=*f9ExCF<ny@$g*qEx^=#@Qt7ZIXTQ2$R4JQ z0+1l_=Y`$mJJ}~vf&w}TNMzctE7sUMI6zbbdA0m8IHmQqwUv#9MbX;q(IXJ`inZs? zoqLnK1CLW1NEENx-qv<$7&<pyQ?T3d9EQKl5vVI&3%3{uzzP8R0XhK;1--*%K7M{8 zeEf2!`TLBstI`A1%@Oezm#?h^@3G>c<Dnv6XJmxdmJbxrKq6XOm%INl;V)%@m6IG7 zcZrzzp@~WQ{VY~CHjJyCx3aLnT#9u6Q(&qCX^U+h9j*HO`SbcEppFdPY8Rne#Ib6> zN=z&V&J1v=rJ?Z;hr3<uU<vjxXI5Rv&sMTV;$&iZxzhLK7`PW?+KU%2LXwr(HU9-R z=*?Q+@Vc)2051#z3J4%S506~V?Cfl)jRno0LNsO_x$aMw1TDz^>f@56b~WO#@Nlrb z-J#fkU<QT?Wf%M_u)3k}+`4-8?YnnUqpesHq~bRS$S4be5bb38=YwhK0PuuD2}u%X ztR4WZw}%e?Bo_cYhzU3_Nbk*EE|U(wfr(Ur&f1z9$^x4IO30?KGs^C)j0k(9o!nEi z9)a@)(vgLUiC86hYHI3ReSLQC0?cag`}9Few#J!4G@P~H=y00D7x+j_R@VI9`XuyY z>)Idy!A!yvH{?`RQ-g^jBXAPT%*ZcXxT=5i=HZ4zIEa^UO5WziM!7uA(6L(fJeo_F zOl@pLfdT+a0mD7`2!tIl6o?>La<C_H&R-!ufKns!s4;3`VWCZg!#jPEQ|EGeuVtMY z<AkpRPyn+O_ZhIWgp%{4byrnCn@hM<WSGaN=5MK^2pYozI@1rKyaT<@i%bBU2Iagc zRGn9Pogmn4;TsGDgi(Uc`=HwA2q=TFp^VDcm3x@qC(qx)MkzQsIYAc!R5efv&>;ip zpYRBE_4O_)Mz0t$m5-sz=(vFGS0P8?cL*rJpqzx6qZw*#Rm#`3a6TjEd@5}bjJJJh z8JSDu<c8Hg-oTw8zSU7lvgY$@jOjAr6KaeIp)ic5O%lSQqlcb|;m;mCgf{#d&oj)= zg29*Og`ahBfqxiTpGAsTmWKAACgv>{cxxHn+QN)uDEJ~ZT<@VD+tR{G&QJ8<`DwyQ zoX%w`cQ|g1+bOG}k;}NyCU&=9z{Yg?5DxGe#09+5euo0ir~H1|-<}KK1_u+=7$s6+ zcClmnO_ESnZtheMUdJzUnp8O%adZwE7Os=<7A>5eDSb0j83uh0VFPTL$Ruzu?`73y zDlaT9*22tMTXM_eD-!K@;AHg0yFE=9?0WFvGo&6sUfDs;nKe1M5mC8em1MwVX=@lm zF$D+hTyN+~TCXLvb04La>Kk6~BO4RyTdZoztpxOc3_R`Pu|sW&CCE&L6Z=P-jcSbG z7G<iW#??blkJo3QEEy^)phtF9m7`@YOeWOUngH>DmX|`mzYC^M@a3BtBY>1ag<Byo zQn!Ui--0fHU1uS3H>Vc{rwqP0NM{L^KfQSK5{E%jR1_Ue+dQLSZDZqy{QMP|oVx;| z8Un%%e0C(uLYmLU=Bp^q4lDB#oio(~AIk(BXTrL~nf3*Rg`w*WQ&CDH&KPG9#3+Nw z%4nr9v=snWnC~0$cbs^=V1hQ_P_c!Q&;i&=^o3CUs`qCSVW#mG9o<jJu4c{WvS4b= zQD>+zf<kRQ-w2j4mkH3d<(V3k_T$G$sBm2xPAoT#<X|iy)q6*qgQEayennMPRrIDI zOl|@D0)_o{t;6U=JrV!kCeaGj{$++O=m=OkG*SS-`}+FeUgO@Av|G4*`Z?+kfx@4a zlOyafC8wyUk}SmQd+epn2VL1ehd&7d;#{x)eXe5wKs){2yCooT%ggX0qoccPG^eSQ zW>gR_d`Cqpy)uU9w&FBz!k2j9)0{j!UZ2HbC<NMW^s*5bYb6`Jc1K**GL?_IQ;#oR zzC4I-1p8IL!i`H*^r$(O1s0c7xa?o&NOxfUlZ2d^f#t!HWMtgg+xLrh%Pp+!-C@r@ z-c&yb{&Og(8f;pSa#LmC^+GGISgiirw>vq#V8y&iOmua-4GkS8Cg_}t0mBE0_KZku z55{Vb!SQ*ZtzCJvGYrr`wtIB`{CVHKT3Q3Q@#l;E1qCqsMsOL{5g6ssQc*j$B&KJG z8BWdD`*J_2Xk9md>tj<XW|Zr24^xvt3gzmQK<lWm>7fNl*2sN#CDRp77?y$&0|pKY zkkJ&6w>&DHW?yAzGq0!lYvy435ZEw;2>7L<r=zVs2!ms|QebJhl@&wLg3A769=f(p zLdkTvFr;YA3MN6h?@1LMF8B`<@8T#ZDCV3{;DixezLgQKqJX@Av>xe;ndE~Z6F@X3 z=h7|i=H_N}Z-LJZTq#(Od-v`^=ami@W+%k#e}4U-nhtIov?!s^rV6|tx8~o?0|s4d zmkmBmmaUXc<B1fxxlzY&a}LHIVRBjxMVzL1H{ljCeDw+N{^Y?ZI$U5pUAO<E6J5AG zH3d`+!xP=~`T5I!js5)=8LfklNLy`NYHJ~9CdWN!QtnumsT)Km2;U{R1a}ExTVG$V z|50&)b)BviR%iG)16$JG(NP!WWD(tFN_)=#o|+ncsg<QAG)e196B4weZ+-JoQ&Y1z zq-V4<DOsj0V11&(lO}wE1M?}EA_DdxNO&+ue>c=%QJR|<U{C^5MJKP6yMYs1@*ZfB z@{b=oy1Ugpn3A2q3;GI*z1m?~0W33+>X;U&KV3|0ENJWC(;^}wAmD~e@?O0nAtH)& zi5>d2-x&;L4|spM6;T7FrKOjspeankkqAQ|mj&$8=wLP#F07zuX=^J6O<+l4%>8>8 z0WyF``?&{(4Re1$#{&YJH~{PP*&y+65YKXfLT~JyoxQ;_y##|>8XCPzO);8DBJN!< z+B-CaPIeu=;0Y(fOa)=O$5cdZppGJqfKNi}A|%<e1PvM<=&6#ai-SXRQ<M81g-m(# z;&#k=p{UCZoE?Gs05cfR*>x4#+;hM|p})}5+zc*7w9$lE)@B1IrT7(IOLed{6rVxs z1uB3U^peD^=P6!*=2Tw>GdN)8yyDz8`U1U7@h*jo;&jtC14mT`Oa=hKQk_!M-`UwA zTCD~S2M!xUU-wg5)ZRo)TwDYtuO-Y;7|+6HfVJTc&KGpQV9>Ch<G`_9R14G4zrXRr zvAwRLA@m4_VZk0WgLVv9W-zJ>qoEpwzQE$(&R|>MQo$i1B*HKQf+1TBXNoI_42oNh z=Xol-9%FP)8ViYxqVOvyRWNjCQ8Khzb<p<5ACgO9p`o5|-Vl&3p`odu2w*Hf^eAts zlF}|`Fj;L8FCDotr9_eJB`;hHQ=3mpDIh$c0}W&CBMIk_<g+ln3OQv^VhNL~?J!=j z(ZmQc4ivUHrt{C?DJ4|{Y7b_0ptOR7pxXhedHu!>cMp#@uV1^lyHg9;ODq-S<>|vf zJ<Q+02gUb(o-T9*r*mRF2_B>n^B@dHTS|XDL^bd@U?I17d8x&Hyc`@p4QT(3yCp~3 zJzni~(`{Wz(ovyci|scffw|Z6&!N10d_IeLWe|#Q2<}zX<o8jLo=v?mZv`a2V+n`0 z<8nxK20Ts`jNHQ925igx!UB2?QOB}mS-D<xU2h%_F*k4e{q+SBl3S+VDUkS*!oVI7 zqL`i>c;w4{p{pD46$uYk;4J_B?^U=G;vlvzU}#ai-$?_E^@3!LfNx@WRARJ_qJ$dQ z=l}P=objK2^@H#p3cjvEVp*btynj<)n)b=bu{YeoDrnvge)vKJlm4lW0%-nk@j{?` zRDzsFgDH9Trv51pQlN5G^>D}YBnW#XBb??G@TLFxmq-57`}=FY^MCu*l>a}^&Mz^4 rs^bO>jlx9tuW#{iVB!F=e?&`lyI7e~i_8%Ihme<6zLPKY(Er~6ZHfTt diff --git a/docs/design/chart_format.md b/docs/design/chart_format.md deleted file mode 100644 index 095ff4829..000000000 --- a/docs/design/chart_format.md +++ /dev/null @@ -1,363 +0,0 @@ -# Helm Charts - -This document describes the Helm chart format, its presentation as an -archive, and its storage and retrieval. - - -* [Changes](#changes) -* [tl;dr: Summary](#tldr-summary) -* [Goals](#goals) - * [Non-Goals](#non-goals) -* [The Chart Format](#the-chart-format) - * [Directory Layout](#directory-layout) - * [The Chart File](#the-chart-file) - * [Releasing a Chart](#releasing-a-chart) -* [The Chart Repository](#the-chart-repository) - * [Repository Protocol](#repository-protocol) - * [Aside: Why a Flat Repository Namespace?](#aside-why-a-flat-repository-namespace) - * [Aside: Why Not Git(Hub) or VCS?](#aside-why-not-github-or-vcs) -* [Chart References: Long form, short form, and local reference](#chart-references-long-form-short-form-and-local-reference) - * [Long Form](#long-form) - * [Short Form](#short-form) - * [Local References](#local-references) -* [References](#references) - - -## Changes - -| Date | Author | Changes | -| ------------|:--------------:|:-------------------------------------------------------| -| 2016-01-21 | mbutcher@deis | Added manifests/ to chart. | -| 2016-01-25 | mbutcher@deis | Added clarifications based on comments. | -| 2016-01-26 | mbutcher@deis | Added hook/, removed manifests/. Added generate header.| -| 2016-03-24 | mbutcher@deis | Updated title and intro of document. | -| 2016-04-01 | dcunnin@google | Updated expander / schema post-DM merge | - - -## tl;dr: Summary -* A **chart** is a folder or archive containing a _Chart.yaml_ file, a _templates/_ directory with one or more template files, supporting files, such as schemas and UI directives, and optionally a _README.md_ and _LICENSE_ file. -* When a chart is **released**, it is tested, versioned, and loaded into a chart repository. -* A **chart repository** is organized according to the conventions of object storage. It is accessible by combining a domain with a repository ID (bucket), and can be browsed. A chart repository contains one or more versioned charts. -* There are three ways to reference charts. A **local reference** references a chart by relative or absolute file system path. It is intended for development. A **long name**, or fully qualified URL, refers to a released chart (in a chart repository) by URL. A **short name**, or mnemonic, is a shortened reference to a chart in a chart repository. A short name can be converted to a **long name** deterministically, and can therefore be used anywhere a long name can be used. - -## Goals -The goal of this document is to define the following aspects of Helm charts: - - -* The **format of a chart** -* The **layout of a chart repository**, with recommendations about implementations -* The format of a **short name** (mnemonic name) of a chart, as well as its fully qualified **long name**, and conventions for referencing local charts instead of short/long names. - - -We assume that we are developing a technology that will enjoy widespread use among developers and operators who… - - -* Are familiar with general Kubernetes design -* Are capable of reading and writing high-level formats like JSON, YAML, and maybe shell scripts -* Have low tolerance for complexity, and are not willing to become domain experts, but… -* Advanced users may be interested in, and willing to, learn advanced features to build much more interesting charts - - -Based on these, our design goal is to make it **easy to find, use, author, release, and maintain charts**, while still making it possible for advanced users to build sophisticated charts. - -This design is based on the integration of Deployment Manager (DM) and Helm, formerly at github.com/deis/helm. It does not lose any of the functionality of DM, and loses only Helm functionality that we believe is disposable. Substantial portions of the chart and template handling logic from the original implementations of these two tools have changed as a result of this integration. The new chart format, described in this document, is one of the primary drivers of those changes. - - -### Non-Goals -This document does not define how either the client or the server must use this data, though we make some non-normative recommendations (e.g. “this data may be displayed to the user” or “a suitable backend will be able to translate this to a fully qualified URL”). - -Consequently, this document does not describe the implementation of either client or server sides of this spec. While it defines the pieces necessary for developing and deploying a chart, it does not define a development workflow. Development workflows are discussed in [another document](../workflow/team-workflows.md). - - -## The Chart Format -We define a Chart as a bundle of data and metadata whose contents are sufficient for installing a workload into a Kubernetes cluster. - - -A Chart is composed of the following pieces: - - -1. A human- and machine-readable metadata file called `Chart.yaml` -2. A directory named `templates/` -3. An optional `README.md` file -4. An optional `LICENSE` file -5. An optional `docs/` directory -7. An optional `icon.svg` - - -A chart _may_ have other directories and files, but such files are not defined by or required by this document. For the purposes of this document, any additional files do not contribute to the core functionality of chart installation. - - -The Chart.yaml file format is described [later in this document](#the-chart-file). - - -The `templates/` directory contains one or more template files, as defined in the [Directory Layout](#directory-layout) section. Templates, along with all of their supporting files, are located therein. - - -An optional `README.md` file may be specified, which contains long-form text information about using this chart. Tools may display this information, if present. The README.md file is in Markdown format, and should contain information about a Chart’s purpose, usage, and development. - - -An optional `LICENSE` file may be present, which specifies license information for this chart and/or the images dependent on it. - - -An optional `docs/` directory may be present, and may contain one or more documentation files. This directory contains documentation that is more specific, verbose, or thorough than what is present in the `README.md` file. - -### Directory Layout -A chart is laid out as follows. The top level directory (represented by the placeholder ROOT) must be the name of the chart (verified by linter). For example, if the chart is named `nginx`, the ROOT directory must be named `nginx/`. - -``` -ROOT/ - Chart.yaml - README.md - LICENSE - docs/ - some.md - templates/ - some.yaml - some.jinja - some.jinja.schema -``` - -Templates are stored in a separate directory for the following reasons: - - -* This future-proofs the format: we can add other directories at the top level and deprecate `templates/`. -* It allows authors to add other files (such as documentation) in a straightforward way that will not confuse definitions-aware tools. -* It allows for the possibility that a chart definition may be embedded inside of project code. - - -Charts must not assume that they can perform file system operations to load another chart or supporting resources directly via the filesystem, nor should they store any operational files outside of the chart directory. This point becomes important in the case where there is Python/Jinja or other executable code inside the chart. These executable components especially should not assume they can access the host filesystem. It should be possible to archive the chart directory (e.g. `tar -cf ROOT.tar ROOT/`) and not lose any necessary information. A chart is an all-encompassing unit that can be processed by the client/server. - -### The Chart File -The `Chart.yaml` file specifies package metadata for the definition. Package metadata is any information that explains the package structure, purpose, and provenance. Its intended audience is tooling that surfaces such information to the user (e.g. a command line client, a web interface, a search engine). - - -A definition file does not specify anything about individual pieces of the definition (e.g. description of per-field schema or metadata), nor does it contain information about the parent environment (e.g. that it is hosted on S3 or in GitHub). Its scope is the definition as a whole. - - -Fields: - - -* name: A human-readable name of the definition. This may contain UTF-8 alphanumeric text, together with dash and underscore characters. -* description: A human-readable description of the definition, not to exceed one paragraph of text. No text formatting is supported. A description may use any printable text characters allowed by the file format. -* version: A SemVer 2 semantic version of the chart (template files). Refer to the [instruction on semver.org](http://semver.org/). -* keywords: A list of human-readable keywords. A keyword may contain alphanumeric text and spaces. -* maintainers: A list of author objects, where an author object has two properties: - * name: Author name - * email: Author email (optional) -* source: A URL to the source code for this chart -* home: A URL to the home page for the project -* expander: Indicates how to process the contents of templates/ (optional) - * name: The name of the expander, as a Kubernetes service name or URL. - * entrypoint: If the expander requires an entrypoint, gives the file (optional). -* schema: The file used to validate the properties (user-configurable inputs) of this chart before expansion. (optional) - -Example: - -``` -name: nginx -description: The nginx web server as a replication controller and service pair. -version: 0.5.1 -keywords: -* https -* http -* web server -* proxy -source: https://github.com/foo/bar -home: http://nginx.com -expander: - name: goexpander-service -schema: Schema.yaml -``` - -### Expanders and Templates -The content of the `templates/` directory and the schema file are defined by the particular expander invoked by name in the Chart.yaml file. Such expanders consume these files, in the context of properties, to generate content. If a schema is given, the expander may use it to validate the properties before expansion. Discussion of the available expanders and how they intepret the content of /templates is outside the scope of this document. - -If no expander is specified, files with yaml or json extensions in the templates/ directory are parsed as Kubernetes API objects and included in the deployment without transformation. Charts may therefore contain Kubernetes API objects that do not contain any parameters or require any server side processing before being sent to Kubernetes. Such charts can therefore not invoke other charts. - -### Releasing a Chart -A chart is _released_ when the source of the chart is tested, versioned, packaged into a gzipped tar file. At that point, that particular release of a chart is considered immutable. No further changes are allowed. To enforce immutability through tamper detection, Charts must be digitally signed. - - -Releases must follow a SemVer 2 version pattern. - - -A released chart may be moved into a chart repository. - - -NON-NORMATIVE: A release pattern _might_ look like this: - -``` -$ helm release -r 1.1.2 ./nginx/ --> generated archive --> signed archive. Signature ‘iEYEARECAAYFAkjil’ --> generated ./nginx-1.1.2.tgz --> uploading to gs://kubernetes-charts/nginx-1.1.2.tgz --> done -``` - -## The Chart Repository -A _Chart Repository_ is a place where _released copies_ of one or more charts may reside. A Helm Chart Repository is analogous to a Debian package repository or a Ruby Gems repository. It is a remote storage location accessible over a well-documented protocol (HTTP(S), and following fixed structural conventions. - - -Chart repositories are styled after the predominant pattern of [object storage systems](https://cloud.google.com/storage/docs/key-terms). A _domain_ hosts one or more _repositories_. A repository is a bucket in which one or more charts may be stored. In an object storage system, this is represented by the pattern: **https://domain/bucket/object**. In object storage, the _object_ part of the URL may contain slashes, hyphens, underscores, and dots. Thus in the URL [https://storage.googleapis.com/helm/nginx-1.1.2] the object is _nginx-1.1.2_. The general pattern of a chart repository is: https://domain/repository/chart - - -A chart name should be of the form _name-version.ext_, where _name_ is the chart name (alpha-numeric plus dash and underscore), and version is a SemVer v2 version. The extension, _ext_, should reflect the type of compression used to archive the object. This specification only discusses gzipped tar archives, but other mechanisms could be supported. - - -Because of the way object storage systems work, a repository should be viewable as a directory tree: - -``` -gs://kubernetes-charts/charts/ - apache-2.3.4.tgz - nginx-1.1.1.tgz - nginx-1.1.2.tgz - redis-0.4.0-alpha.1.tgz -``` - -A helm chart is a gzip-compressed tar archive (e.g. `tar -zcf …`). - -### Repository Protocol -A repository must implement HTTP(S) GET operations on both chart names (nginx-1.2.3) and chart signatures (nginx-1.2.3.sig). HTTP GET is a rational base level of functionality because it is well understood, nearly ubiquitous, and simple. - - -A repository may implement the full Object Storage APIs for compatibility with S3 and Google Cloud Storage. In this case, a client may detect this and use those APIs to perform create, read, update, delete, and list operations (as permissions allow) on the object storage system. - - -Object storage is a rational choice for this model because it is [optimized for highly resilient, distributed, highly available read-heavy traffic](https://en.wikipedia.org/wiki/Object_storage). The S3-like API has become a de facto standard, with myriad client libraries and tools, and major implementations both as services (S3, GCS), platforms (OpenStack Swift), and individual stand-alone servers (RiakCS, Minio, Ceph). - - -This document does not mandate a particular authentication mechanism, but implementors may implement the authentication token mechanisms on GCS and S3 compatible object storage systems. - -### Aside: Why a Flat Repository Namespace? -The format for a repository has packages stored in a flat namespace, with version information appended to the filename. There are a few reasons we opted for this over against more hierarchical directory structures: - - -* Keeping package name and version in the file name makes the experience easier for development. In cases where manual examination is necessary, tools and humans are working with filenames that are easily identifiable (_nginx-1.2.3.tgz_). -* The flat namespace means tooling needs to pay less attention to directory hierarchy, which has three positive implications: - * Less directory recursion, especially important for base operations like counting charts in a repo. - * No importance is granted to directories, which means we could introduce non-semantic directory hierarchy if necessary (e.g. for the GitHub model of _username/reponame_. - * For object storage systems, where slashes are part of names but with special meaning, the flat namespace reduces the number of API operations that need to be done, and allows for using some of the [object storage URL components](https://cloud.google.com/storage/docs/json_api/v1/) when necessary. -* Brevity is desirable when developers have occasion to type these URLs directly, as is done in chart/template references (see, for example, the [S3 API](http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingObjects.html)). - -### Aside: Why Not Git(Hub) or VCS? -GitHub-backed repositories for released charts suffer from some severe limitations (listed below). Our repository design does not have these limitations. - - -* Versioning Multiple Things - * Git does not provide a good way of versioning independent objects in the same repo. (Two attempts we made at solving this [here](https://github.com/helm/helm/issues/199) and [here](https://github.com/deis/helm-dm/issues/1)) - * A release should be immutable, or very close to it. Achieving this in Git is hard if the thing released is not the entire repository. -* Developer Experience - * Combining release artifacts and source code causes confusion - * Substantial development workflow overhead (and over-reliance on conventions) make directory-based versioning problematic - * In most of the models specified, there is no way of determining whether a resource is in development, or is complete -* Infrastructure - * Both teams have hit GitHub API rate limiting - * Git is optimized for fetching an entire repository, not a small fraction of the repository - * Git is optimized for fetching an entire history, not a snapshot of a particular time - * Discovery of which versions are available is exogenous to Git itself (unless we use a crazy tagging mechanism) - * We hit vendor lock-in for GitHub - - -The object storage based repository solution above solves all of these problems by: - - -* Explicitly defining a release artifact as an archive file with a known format and a known naming convention. -* Explicitly defining a release as an act of archiving, naming, and signing. -* Selecting a service (object storage, but with fallback to plain HTTP) that is resilient, widely deployed, and built specifically for the delivery of release files. -* Distinguishing the development workflow from the release workflow at a location that is intuitive and common for developers -* Providing a method for verifying immutability of a version by checksum -* Removing vendor reliance -* Separating the concept of code from the concept of release artifact - - -We want to make clear that a chart may be developed in any place (such as Github repositories). This aside is referring to released and versioned charts that can be stored and shared. We do not dictate where a chart is developed. - -## Chart References: Long form, short form, and local reference -There are three reference forms for a chart. A fully qualified (long) form, a mnemonic (short) form, and a local path spec. -### Long Form -A long form chart reference is an exact URL to the chart. It should use HTTP or HTTPS protocols, and follow the typical rules of a complete URL: https://example.com/charts/nginx-1.2.3.tgz - - -A long form reference must include a protocol spec. It may also contain any pattern allowed in URLs. - - -The following URI schemes must be supported: -* http: -* https: - - -#### Special Form Google Storage and S3 -The [Google Storage scheme](https://cloud.google.com/storage/docs/interoperability) (`gs://`) and the S3 scheme (`s3://`) can also be supported. These forms do not follow the URL standard (bucket name is placed in the host field), but their behavior is well-documented. - - -Examples: -``` -gs://charts/nginx-1.2.3.tgz -s3://charts/nginx-1.2.3.tgz -``` -### Short Form -A short form package reference provides the minimum necessary information for accessing a chart. Short form URLs use a defined [URI scheme](https://tools.ietf.org/html/rfc3986#section-3.1) of `helm`:. - - -A generic short form maps to an object storage reference (`DOMAIN/REPOSITORY/RELEASE`). - -``` -helm:example.com/charts/nginx-1.2.3.tgz -``` - -Or, in the case of the Google Storage (`gs://`) and the S3 scheme (`s3://`), the domain indicates the storage scheme. - -``` -helm:gs/kubernetes-charts/nginx-1.2.3.tgz -``` - -For the purpose of providing versioning ranges, and also for backward compatibility, version requirements are expressed as a suffix condition, instead of as part of the path: - -``` -helm:example.com/charts/nginx#1.2.3 // Exact version -helm:example.com/charts/nginx#~2.1 // Version range -helm:example.com/charts/nginx // Latest release -``` - -The first of the above three short names is equivalent to helm:example.com/charts/nginx-1.2.3.tgz. The second example uses a semantic version filter ~1.2, which means “>=1.2.0, <1.3.0”, or “any patch version of 1.2”. Other filters include the “^” operator (^1 is “any minor version in the 1.x line), and “>”, “>=”, “=”, “<”, and “<=” operators. These are standard SemVer filter operators. - - -Any short form handler should be able to resolve the default short form as specified above. - - -### Local References -During chart development, and in other special circumstances, it may be desirable to work with an unversioned, unpackaged local copy of a chart. For the sake of consistency across products, an explicit naming formula is to be followed. - - -In cases where a local path is used in lieu of a full or short name, the path string _must_ begin with either a dot (.) or a slash (/) or the file schema(`file:`). - - -Legal examples of this include: -``` - ./foo - ../foo - /foo - //foo - /.foo - file:///example/foo -``` - -Unprefixed relative paths are not valid. For example, `foo/` is not allowed as a local path, as it conflicts with a legal short name, and is thus ambiguous. - - -## References - -The Debian Package Repo. [http://ftp.us.debian.org/debian/pool/main/h/] - -The Debian Maintainers Guide. [https://www.debian.org/doc/manuals/maint-guide/] - -Arch packages: [https://wiki.archlinux.org/index.php/Arch_User_Repository#Creating_a_new_package] - -Keybase.io: [https://keybase.io/] - -Google Cloud Storage API: [https://cloud.google.com/storage/docs/json_api/v1/] - -Amazon S3: [http://docs.aws.amazon.com/AmazonS3/latest/dev/Welcome.html] - -URIs (RFC 3986): [https://tools.ietf.org/html/rfc3986#section-3.1] diff --git a/docs/design/design.md b/docs/design/design.md deleted file mode 100644 index 290bdceed..000000000 --- a/docs/design/design.md +++ /dev/null @@ -1,434 +0,0 @@ -# Deployment Manager Design - -## Overview - -Deployment Manager (DM) is a service that runs in a Kubernetes cluster, -supported by a command line interface. It provides a declarative `YAML`-based -language for configuring Kubernetes resources, and a mechanism for deploying, -updating, and deleting configurations. This document describes the configuration -language, the API model, and the service architecture in detail. - -## Configuration Language - -DM uses a `YAML`-based configuration language with a templating mechanism. A -configuration is a `YAML` file that describes a list of resources. A resource has -three properties: - -* `name`: the name to use when managing the resource -* `type`: the type of the resource being configured -* `properties`: the configuration properties of the resource - -Here's a snippet from a typical configuration file: - -``` -resources: -- name: my-rc - type: ReplicationController - properties: - metadata: - name: my-rc - spec: - replicas: 1 - ... -- name: my-service - type: Service - properties: - ... -``` - -It describes two resources: - -* A replication controller named `my-rc`, and -* A service named `my-service` - -## Types - -Resource types are either primitives or templates. - -### Primitives - -Primitives are types implemented by the Kubernetes runtime, such as: - -* `Pod` -* `ReplicationController` -* `Service` -* `Namespace` -* `Secret` - -DM processes primitive resources by passing their properties directly to -`kubectl` to create, update, or delete the corresponding objects in the cluster. - -(Note that DM runs `kubectl` server side, in a container.) - -### Templates - -Templates are abstract types created using Python or -[Jinja](http://jinja.pocoo.org/). A template takes a set of properties as input, -and must output a valid `YAML` configuration. Properties are bound to values when -a template is instantiated by a configuration. - -Templates are expanded before primitive resources are processed. The -configuration produced by expanding a template may contain primitive resources -and/or additional template invocations. All template invocations are expanded -recursively until the resulting configuration is a list of primitive resources. - -(Note, however, that DM preserves the template hierarchy and any dependencies -between resources in a layout that can be used to reason programmatically about -the structure of the resulting collection of resources created in the cluster, -as described in greater detail below.) - -Here's an example of a template written in [Jinja](http://jinja.pocoo.org/): - -``` -resources: -- name: {{ env['name'] }}-service - type: Service - properties: - prop1: {{ properties['prop1'] }} - ... -``` - -As you can see, it's just a `YAML` file that contains expansion directives. For -more information about the kinds of things you can do in a Jinja based template, -see [the Jina documentation](http://jinja.pocoo.org/docs/). - -Here's an example of a template written in Python: - -``` -import yaml - -def GenerateConfig(context): - resources = [{ - 'name': context.env['name'] + '-service', - 'type': 'Service', - 'properties': { - 'prop1': context.properties['prop1'], - ... - } - }] - - return yaml.dump({'resources': resources}) -``` - -Of course, you can do a lot more in Python than in Jinja, but basic things, such -as simple parameter substitution, may be easier to implement and easier to read in -Jinja than in Python. - -Templates provide access to multiple sets of data that can be used to -parameterize or further customize configurations: - -* `env`: a map of key/value pairs from the environment, including pairs -defined by Deployment Manager, such as `deployment`, `name`, and `type` -* `properties`: a map of the key/value pairs passed in the properties section -of the template invocation -* `imports`: a map of import file names to file contents for all imports -originally specified for the configuration - -In Jinja, these variables are available in the global scope. In Python, they are -available as properties of the `context` object passed into the `GenerateConfig` -method. - -### Template schemas - -A template can optionally be accompanied by a schema that describes it in more -detail, including: - -* `info`: more information about the template, including long description and title -* `imports`: any files imported by this template (may be relative paths or URLs) -* `required`: properties that must have values when the template is expanded -* `properties`: A `JSON Schema` description of each property the template accepts - -Here's an example of a template schema: - -``` -info: - title: The Example - description: A template being used as an example to illustrate concepts. - -imports: -- path: helper.py - -required: -- prop1 - -properties: - prop1: - description: The first property - type: string - default: prop-value -``` - -When a schema is provided for a template, DM uses it to validate properties -passed to the template by its invocation, and to provide default values for -properties that were not given values. - -Schemas must be supplied to DM along with the templates they describe. - -### Supplying templates - -Templates can be supplied to DM in two different ways: - -* They can be passed to DM along with configurations that import them, or -* They can be retrieved by DM from public HTTP endpoints for configurations that -reference them. - -#### Template imports - -Configurations can import templates using path declarations. For example: - -``` -imports: -- path: example.py - -resources: -- name: example - type: example.py - properties: - prop1: prop-value -``` - -The `imports` list is not understood by the Deployment Manager service. -It's a directive used by client-side tools to specify what additional files -should be included when passing the configuration to the API. - -If you are calling the Deployment Manager service directly, you must embed the -imported templates in the configuration passed to the API. - -#### Template references - -Configurations can also reference templates using URLs for public HTTP endpoints. -DM will attempt to resolve template references during expansion. For example: - -``` -resources: -- name: my-template - type: https://raw.githubusercontent.com/my-template/my-template.py - properties: - prop1: prop-value -``` - -When resolving template references, DM assumes that templates are stored in -directories, which may also contain schemas, examples and other supporting files. -It therefore processes template references as follows: - -1. Attempt to fetch the template, and treat it as an import. -1. Attempt to fetch the schema for the template from -`<base path>/<template name>.schema` -1. Attempt to fetch files imported by the schema from `<base path>/<import path>` - -Referring to the previous example, - -* the base path is `https://raw.githubusercontent.com/my-template`, -* the template name is `my-template`, and -* the schema name is `my-template.schema` - -If we include a configuration that uses the template as an example, then the -directory that contains `my-template` might look like this: - -``` -example.yaml -my-template.py -my-template.py.schema -helper.py -``` - -### Value references -Resources can reference values from other resources. The version of Deployment -Manager running in the Google Cloud Platform uses references to understand -dependencies between resources and properly order the operations it performs on -a configuration. - -(Note that this version of DM doesn't yet order operations to satisfy -dependencies, but it will soon.) - -A reference follows this syntax: `$(ref.NAME.PATH)`, where `NAME` is the name -of the resource being referenced, and `PATH` is a `JSON` path to the value in the -resource object. - -For example: - -``` -$(ref.my-service.metadata.name) -``` - -In this case, `my-service` is the name of the resource, and `metadata.name` is -the `JSON` path to the value being referenced. - -## API Model - -DM exposes a set of RESTful collections over HTTP/JSON. - -### Deployments - -Deployments are the primary resources managed by the Deployment Manager service. -The inputs to a deployment are: - -* `name`: the name by which the deployment can be referenced once created -* `configuration`: the configuration file, plus any imported files (templates, -schemas, helper files used by the templates, etc.). - -Creating, updating or deleting a deployment creates a new manifest for the -deployment. When deleting a deployment, the deployment is first updated to -an empty manifest containing no resources, and then removed from the system. - -Deployments are available at the HTTP endpoint: - -``` -http://manager-service/deployments -``` - -### Manifests - -A manifest is created for a deployment every time it is changed. It contains -three key components: - -* `inputConfig`: the original input configuration for the manifest -* `expandedConfig`: the expanded configuration describing only primitive resources -* `layout`: the hierarchical structure of the configuration - -Manifests are available at the HTTP endpoint: - -``` -http://manager-service/deployments/<deployment>/manifests -``` - -#### Expanded configuration - -Given a new `inputConfig`, DM expands all template invocations recursively, -until the result is a flat set of primitive resources. This final set is stored -as the `expandedConfig` and is used to instantiate the primitive resources. - -#### Layout - -Using templates, callers can build rich, deeply hierarchical architectures in -their configurations. Expansion flattens these hierarchies to simplify the process -of instantiating the primitive resources. However, the structural information -contained in the original configuration has many potential uses, so rather than -discard it, DM preserves it in the form of a `layout`. - -The `layout` looks a lot like the original configuration. It is a `YAML` file -that describes a list of resources. Each resource contains the `name`, `type` -and `properties` from the original configuration, plus a list of nested resources -discovered during expansion. The resulting structure looks like this: - -* name: name of the resource -* type: type of the resource -* properties: properties of the resource, set only for templates -* resources: sub-resources from expansion, set only for templates - -Here's an example of a layout: - -``` -resources: -- name: rs - type: replicatedservice.py - propertes: - replicas: 2 - resources: - - name: rs-rc - type: ReplicationController - - name: rs-service - type: Service -``` - -In this example, the top level resource is a replicated service named `rs`, -defined by the template named `replicatedservice.py`. Expansion produced the -two nested resources: a replication controller named `rs-rc`, and a service -named `rs-service`. - -Using the layout, callers can discover that `rs-rc` and `rs-service` are part -of the replicated service named `rs`. More importantly, if `rs` was created by -the expansion of a larger configuration, such as one that described a complete -application, callers could discover that `rs-rc` and `rs-service` were part of -the application, and perhaps even that they were part of a RabbitMQ cluster in -the application's mid-tier. - -### Types -The types API provides information about types used in the cluster. - -It can be used to list all known types used by active deployments: - -``` -http://manager-service/types -``` - -Or to list all active instances of a specific type in the cluster: - -``` -http://manager-service/types/<type>/instances -``` - -Passing `all` as the type name shows all instances of all types in the -cluster. The following information is reported for type instances: - -* name: name of resource -* type: type of resource -* deployment: name of deployment in which the resource resides -* manifest: name of manifest in which the resource configuration resides -* path: JSON path to the entry for the resource in the manifest layout - -## Architecture - -The Deployment Manager service is manages deployments within a Kubernetes -cluster. It has three major components. The following diagram illustrates the -components and the relationships between them. - - - -Currently, there are two caveats in the service implementation: - -* Synchronous API: the API currently blocks on all processing for - a deployment request. In the future, this design will change to an - asynchronous operation-based mode. -* In-memory state: the service currently stores all state in memory, - so it will lose all knowledge of deployments and related objects on restart. - In the future, the service will persist all state in the cluster. - -### Manager - -The `manager` service acts as both the API server and the workflow engine for -processing deployments. It handles a `POST` to the `/deployments` collection as -follows: - -1. Create a new deployment with a manifest containing `inputConfig` from the - user request -1. Call out to the `expandybird` service to expand the `inputConfig` -1. Store the resulting `expandedConfig` and `layout` -1. Call out to the `resourcifier` service to instantiate the primitive resources -described by the `expandedConfig` -1. Respond with success or error messages to the original API request - -`GET`, `PUT` and `DELETE` operations are processed in a similar manner, except -that: - -* No expansion is performed for `GET` or `DELETE` -* The primitive resources are updated for `PUT` and deleted for `DELETE` - -The manager is responsible for saving the information associated with -deployments, manifests, type instances, and other resources in the Deployment -Manager model. - -### Expandybird - -The `expandybird` service takes in a configuration, performs all necesary -template expansions, and returns the resulting flat configuration and layout. -It is completely stateless. - -Because templates are written in Python or Jinja, the actual expansion process -is performed in a sub-process that runs a Python interpreter. A new sub-process -is created for every request to `expandybird`. - -Currently, expansion is not sandboxed, but templates should be reproducable, -hermetically sealed entities. Future designs may therefore introduce a sandbox to -limit external interaction, such as network or disk access, during expansion. - -### Resourcifier - -The `resourcifier` service takes in a flat expanded configuration describing -only primitive resources, and makes the necessary `kubectl` calls to process -them. It is totally stateless, and handles requests synchronously. - -The `resourcifier` runs `kubectl` in a sub-process within its container. A new -sub-process is created for every request to `resourcifier`. - -It returns either success or error messages encountered during resource processing. diff --git a/docs/design/provenance_proposal.md b/docs/design/provenance_proposal.md deleted file mode 100644 index 5eb1d3dc1..000000000 --- a/docs/design/provenance_proposal.md +++ /dev/null @@ -1,67 +0,0 @@ -_Note: When a chart file is deployed, a [provenance file](#the-provenance-file) is generated for the chart. That file is not stored inside of the chart, but is considered part of the chart’s packaged format._ - -Testing and provenancing attach badges to the Chart that attest to its quality and provenance. - -### The Provenance File - -The provenance file contains a chart’s YAML file plus several pieces of verification information. Provenance files are designed to be automatically generated. - - -The following pieces of provenance data are added: - - -* The chart file (Chart.yaml) is included to give both humans and tools an easy view into the contents of the chart. -* Every image file that the project references is checksummed (SHA-256?), and the sum included here. If two versions of the same image are used by the template, both checksums are included. -* The signature (SHA-256) of the chart package (the .tgz file) is included, and may be used to verify the integrity of the chart package. -* The entire body is signed using PGP (see [http://keybase.io] for an emerging way of making crypto signing and verification easy). - - -The combination of this gives users the following assurances: - - -* The images this chart references at build time are still the same exact version when installed (checksum images). - * This is distinct from asserting that the image Kubernetes is running is exactly the same version that a chart references. Kubernetes does not currently give us a way of verifying this. -* The package itself has not been tampered with (checksum package tgz). -* The entity who released this package is known (via the GPG/PGP signature). - - -The format of the file is as follows: - -``` ------BEGIN PGP SIGNED MESSAGE----- -name: nginx -description: The nginx web server as a replication controller and service pair. -version: 0.5.1 -keywords: - - https - - http - - web server - - proxy -source: https://github.com/foo/bar -home: http://nginx.com -depends: - kubernetes: - version: >= 1.0.0 ---- -files: - nginx-0.5.1.tgz: “sha256:9f5270f50fc842cfcb717f817e95178f” -images: - “hub.docker.com/_/nginx:5.6.0”: “sha256:f732c04f585170ed3bc99” ------BEGIN PGP SIGNATURE----- -Version: GnuPG v1.4.9 (GNU/Linux) - - -iEYEARECAAYFAkjilUEACgQkB01zfu119ZnHuQCdGCcg2YxF3XFscJLS4lzHlvte -WkQAmQGHuuoLEJuKhRNo+Wy7mhE7u1YG -=eifq ------END PGP SIGNATURE----- -``` - -Note that the YAML section contains two documents (separated by ---\n). The first is the Chart.yaml. The second is the checksums, defined as follows. - - -* Files: A map of filenames to SHA-256 checksums (value shown is fake/truncated) -* Images: A map of image URLs to checksums (value shown is fake/truncated) - - -The signature block is a standard PGP signature, which provides [tamper resistance](http://www.rossde.com/PGP/pgp_signatures.html). diff --git a/docs/design/user_stories.md b/docs/design/user_stories.md deleted file mode 100644 index a63b6e9ac..000000000 --- a/docs/design/user_stories.md +++ /dev/null @@ -1,33 +0,0 @@ -## Appendix A: User Stories for Charts - -Personas: - -- Operator: Responsible for running an application in production. -- Chart Dev: Responsible for developing new charts -- App Dev: Developer who creates applications that make use of existing charts, but does not create charts. - - -Stories: - -- As an operator, I want a deployment that is 100% reproducible (exact versions) -- As an app dev, I want to be able to search for charts using keys defined in the [chart file](#the-chart-file)... - * by keyword, where one app may have multiple keywords (e.g. Redis has storage, message queue) - * by name (meaning name of the chart), where name may be "fuzzy". - * by author - * by last updated date -- As a chart dev, I want a well-defined set of practices to follow -- As a chart dev, I want to be able to work with a team on the same chart -- As a chart dev, I want to be able to indicate when a particular chart is stable, and how stable it is -- As a chart dev, I want to indicate the role I played in building a chart -- As a chart dev, I want to be able to use all of the low-level Kubernetes kinds -- As an operator, I want to be able to determine how stable a package is -- As an operator, I want to be able to determine what version of Kubernetes I need to run a chart -- As an operator, I want to determine whether a chart requires extension kinds (e.g. DaemonSet or something custom), and determine this _before_ I try to install -- As a chart dev, I want to be able to express that my chart depends on others, even to the extent that I specify the version or version range of the other chart upon which I depend -- As a chart dev, I do not want to install additional tooling to write, test, or locally run a chart (this relates to the file format in that the format should not require additional tooling) -- As a chart dev, I want to be able to store auxiliary files of arbitrary type inside of a chart (e.g. a PDF with documentation) -- As a chart dev, I want to be able to store my chart in one repository, but reference a chart in another repository -- As a chart dev, I want to embed my template inside of the code that it references. For example, I want to have the code to build a docker image and the chart to all live in the same source code repository. - - - diff --git a/docs/pushing.md b/docs/pushing.md deleted file mode 100644 index 3f189839d..000000000 --- a/docs/pushing.md +++ /dev/null @@ -1,22 +0,0 @@ -# Pushing Helm - -This details the requirements and steps for doing a `helm` push. - -## Prerequisites - -In order to build and push `helm`, you must: - -* be an editor or owner on the GCP project `kubernetes-helm` -* have `docker` installed and runnable in your current environment -* have `gcloud` installed -* have `gsutil` installed - -## Pushing - -To build and push the service containers and the client binaries for all -supported platforms and architectures, checkout the branch and tag you intend to release, -and then run the following: - -``` -$ DOCKER_PROJECT=kubernetes-helm make push -``` diff --git a/docs/pushing_charts.md b/docs/pushing_charts.md deleted file mode 100644 index 5c7b5040d..000000000 --- a/docs/pushing_charts.md +++ /dev/null @@ -1,64 +0,0 @@ -# Pushing Charts - -This details the current requirements and steps for pushing a chart to -Google Cloud Storage (GCS) using gsutil tool. - -## Prerequisites - -In order to create and push a Chart, you must: -* have a bucket in GCS with write permissions -* have [gsutil](https://cloud.google.com/storage/docs/gsutil) tool configured and installed - -## Creating a chart (optional) - -If you already have a [chart](./design/chart_format.md) in the file system format, you can -skip this step. -./bin/helm chart create mychart - -This will create the following directory structure: -vaikas@vaikas-glaptop:~/projects/dmos/src/github.com/kubernetes/helm$ find mychart -mychart -mychart/Chart.yaml -mychart/hooks -mychart/templates -mychart/docs - -You can then create your own chart (examples/charts has examples to get you started) - -## Serializing the chart - -Helm tool can package a chart for you in the correct format and with name that -matches the version in the Chart.yaml file. - -``` -./bin/helm chart package <yourchart> -``` - -Using one of the examples [nginx](examples/charts/nginx/Chart.yaml) - -``` -./bin/helm chart package examples/charts/nginx -``` - -The resulting file will be nginx-0.0.1.tgz - -## Pushing the chart - -Using gsutil you can copy this file to your bucket with the following command: -``` -gsutil cp <yourchart> gs://<yourbucket>/ -``` -or if you want to make this chart publicly readable: -``` -gsutil cp -a public-read <yourchart> gs://<yourbucket>/ -``` - -To handle more granular permissions, use the 'gsutil help acls' command. - -Using the example above, you could make this publicy readable by doing: -``` -gsutil cp -a public-read nginx-0.0.1.tgz gs://<yourbucket>/ -``` - - - diff --git a/docs/templates/registry.md b/docs/templates/registry.md deleted file mode 100644 index 5f295cfd6..000000000 --- a/docs/templates/registry.md +++ /dev/null @@ -1,289 +0,0 @@ -# Template Registries - -DM lets configurations instantiate [templates](../design/design.md#templates) -using both [imports](../design/design.md#template-imports) and -[references](../design/design.md#template-references). - -Because template references can use any public HTTP endpoint, they provide -a way to share templates. While you can store templates anywhere you want and -organize them any way you want, you may not be able to share them effectively -without some organizing principles. This document defines conventions for -template registries that store templates in Github and organize them by name -and by version to make sharing easier. - -For a working example of a template registry, please see the -[Kubernetes Template Registry](https://github.com/kubernetes/application-dm-templates). - -## Template Versions - -Since templates referenced by configurations and by other templates may change -over time, we need a versioning scheme, so that template references can be reliably -resolved to specific template versions. - -Every template must therefore carry a version based on the -[Semantic Versioning](http://semver.org/) specification. A template version -consists of a MAJOR version, a MINOR version and a PATCH version, and can -be represented as a three part string starting with the letter `v` and using -dot delimiters between the parts. For example `v1.1.0`. - -Parts may be omitted from right to left, up to but not include the MAJOR -version. All omitted parts default to zero. So, for example: - -* `v1.1` is equivalent to `v1.1.0`, and -* `v2` is equivalent to `v2.0.0` - -As required by Semantic Versioning: - -* The MAJOR version must be incremented for incompatible changes -* The MINOR version must be incremented functionality is added in a backwards-compatible -manner, and -* The PATCH version must be incremented for backwards-compatible bug fixes. - -When resolving a template reference, DM will attempt to fetch the template with -the highest available PATCH version that has the same MAJOR and MINOR versions as -the referenced version. However, it will not automatically substitute a higher -MINOR version for a requested MINOR version with the same MAJOR version, since -although it would be backward compatible, it would not have the same feature set. -You must therefore explicitly request the higher MINOR version in this situation -to obtain the additional features. - -## Template Validation - -Every template version should include a configuration named `example.yaml` -that can be used to deploy an instance of the template. This file, along with -any supporting files it requires, may be used automatically in the future by -a template testing framework to validate the template, and should therefore be -well formed. - -## Template Organization - -Technically, all you need to reference a template is a directory at a public -HTTP endpoint that contains a template file named either `<template-name>.py` -or `<template-name>.jinja`, depending on the implementation language, along -with any supporting files it might require, such as an optional schema file -named `<template-name>.py.schema` or `<template-name>.jinja.schema`, respectively, -helper files used by the implementation, files imported by the schema, and so on. - -### Basic structure - -These constraints impose a basic level of organization on the template definition -by ensuring that the template and all of its supporting files at least live in the -same directory, and that the template and schema files follow well-defined naming -conventions. - -They do not, however, provide any encapsulation. Without additional constraints, -there is nothing to prevent template publishers from putting multiple templates, -or multiple versions of the same template, in the same directory. While there -might be some benefits in allowing templates to share a directory, such as avoiding -the duplication of helper files, the cost of discovering and maintaining templates -would quickly outweigh them as the number of templates in the directory increased. - -Also, since it may reduce management overhead to store many different templates, -and/or many versions of the same template, in a single repository, we need a way -to organize templates within a repository. - -Therefore: - -* Every template version must live in its own directory named for the version. -* The version directory must contain exactly one top-level template file and -supporting files for exactly one template version. -* All of the versions of a given template must live under a directory named for -the template without extensions. - -For example: - -``` -templateA/ - v1/ - example.yaml - templateA.py - templateA.py.schema - v1.0.1/ - example.yaml - templateA.py - templateA.py.schema - v1.1/ - example.yaml - templateA.py - templateA.py.schema - helper.py -``` - -In this example, `templateA` is a template directory, and `v1`, `v1.01`, and -`v1.1` are template version directories that hold the versions of `templateA`. - -### Registry based template references - -In general, -[templates references](https://github.com/kubernetes/helm/blob/master/docs/design/design.md#template-references) -are just URLs to HTTP endpoints. However, because a template registry follows -the conventions outlined above, references to templates in a template registry -can be shorter and simpler than generalized template references. - -In a registry based template reference, the scheme part of the URL and the name -of the top level template file are omitted, and the version number is delimited -by a colon. So for example, instead of - -``` -https://raw.githubusercontent.com/ownerA/repository2/master/templateA/v1/templateA.py -``` - -you can simply write - -``` -github.com/ownerA/repository2/templateA:v1 -``` - -The general pattern for a registry based template reference is as follows: - -``` -github.com/<owner>/<repository>/<collection>/<template>:<version> -``` - -The `collection` segment, described below, is optional, and may be omitted. - -### Grouping templates - -Of course, a flat list of templates won't scale, and it's unlikely that any -fixed taxonomy would work for all registries. Template directories may therefore -be grouped in any way that makes sense to the repository maintainers. - -For example, this flat list of template directories is valid: - -``` -templates/ - templateA/ - v1/ - ... - templateB/ - v2/ - ... -``` - -This example, where template directories are organized by category, is also valid: - -``` -templates/ - big-data/ - templateA/ - v1/ - ... - templateB/ - v2/ - ... - signals - templateC/ - v1/ - ... - templateD/ - v1.1/ - ... -``` - -### Template collections - -A side effect of allowing arbitrary grouping is that we don't know how to find -templates when searching or listing the contents of a registry without walking -the directory tree down to the leaves and then backtracking to identify template -directories. - -Since walking the repository is not very efficient, we introduce the concept of -collections. - -#### Definition - -A collection is a directory that contains a flat list of templates. Deployment -manager will only discover templates at the root of a collection. - -So for example, `templateA` and `templateB` live in the `templates` collection -in the first example above, and in the `big-data` collection in the second example. - -A registry may contain any number of collections. A single, unnamed collection -is implied at the root of every registry, but additional named collections may -be created at other points in the directory structure. - -#### Usage - -Of course, collections are useless if we can't reference them efficiently. A -registry based template reference may therefore include a collection name. A -collection name is the only path segment allowed between the repository name and -the template name. So, for example, this is a valid template reference: - -``` -github.com/ownerA/repository2/collectionM/templateA:v1 -``` - -but this is not: - -``` -github.com/ownerA/repository2/multiple/path/segments/are/not/allowed/templateA:v1 -``` - -Because it may appear in a template reference, a collection name must not contain -URL path separators (i.e., slashes). However, it may contain other delimiters -(e.g., dots). So, for example, this is a valid template reference: - -``` -github.com/ownerA/repository2/dot.delimited.strings.are.allowed/templateA:v1 -``` - -#### Mapping - -Currently, deployment manager maps collection names to top level directory names. -This mapping implies that registries can be at most one level deep. Soon, however, -we plan to introduce a metadata file at the top level that maps collection names -to paths. This feature will allow registries to have arbitrary organizations, by -making it possible to place collections anywhere in the directory tree. - -When the metadata file is introduced, the current behavior will be the default. -So, if the metadata file is not found in a given registry, or if a given collection -name is not found in the metadata file, then deployment manager will simply map -it to a top level directory name by default. This approach allows us to define -collections at the top level now, and then move them to new locations later without -breaking existing template references. - -## Using Template Registries - -### Accessing a template registry - -The Deployment Manager client, `dm`, can deploy templates directly from a registry -using the following command: - -``` -$ dm deploy <template-name>:<version> -``` - -To resolve the template reference, `dm` looks for a template version directory -with the given version in the template directory with the given template name in -the default template registry. - -The default is the [Kubernetes Template Registry](https://github.com/kubernetes/application-dm-templates), -but you can set a different default using the `--registry` flag: - -``` -$ dm --registry my-org/my-repo/my-collection deploy <template-name>:<version> -``` - -Alternatively, you can specify a complete template reference using the pattern -described above, like this: - -``` -$ dm deploy github.com/my-org/my-repo/my-collection/<template-name>:<version> -``` - -If a template requires properties, you can provide them on the command line: - -``` -$ dm --properties prop1=value1,prop2=value2 deploy <template-name>:<version> -``` - -### Changing a template registry - -DM relies on Github to provide the tools and processes needed to add, modify or -delete the contents of a template registry. Conventions for changing a template -registry are defined by the registry maintainers, and should be published in the -top level README.md or a file it references, following standard Github practices. - -The [Kubernetes Template Registry](https://github.com/kubernetes/application-dm-templates) -follows the [workflow](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/development.md#git-setup) -used by Kubernetes. diff --git a/docs/test-architecture.md b/docs/test-architecture.md deleted file mode 100644 index d44e2abcc..000000000 --- a/docs/test-architecture.md +++ /dev/null @@ -1,59 +0,0 @@ -Testing via Helm -================ - -Problem Summary ---------------- - -Currently in helm/charts we have a simple way of health-checking charts which -checks that all pods in the chart reach "Running" state on a running Kubernetes -cluster. In order to build a system that holds chart quality at the forefront, -we need to introduce an easy way to create and run potentially more complex -application-centric tests. - -Proposed Solution ------------------ - -#### User Experience - -``` -# helm/dm UX for installing charts stays the same - -# addition to UX giving easy-to-run tests: -helm test deis - -``` - -#### Helm's new `test` Command - -This command uses test logic located in `<chart name>/tests/` to ensure that -the chart is operating as intended in an end to end or system test sort of -fashion. - -The command is fairly simple - all it does is load a set of charts into a -Kubernetes cluster (might be the same or different as the deployed chart). -What usually makes sense to have is a one-off pod that runs to completion and -exits with a certain exit code: non-zero signifying failure or zero signifying -success. The pod is not automatically restarted so that humans or automated -tools can inspect the results which might be a log, test artifact, or something -else. `helm test` should therefore be able to be rerun the tests over and over and -not be hindered by an existing similarly-named set of pods, rcs, or services. - -By forcing tests to also be containerized and Kubernetes-ready we have the -benefit of having a single and easily understandable entry point - it's just -another set of Kubernetes components. - -#### Modifications to Chart Structure - -``` -ROOT/ - Chart.yaml - LICENSE - README.md - ... - tests/ - Chart.yaml - templates/ - some.yaml - some.jinja - some.jinja.schema -``` diff --git a/docs/usage_docs/authoring_charts.md b/docs/usage_docs/authoring_charts.md deleted file mode 100644 index 1f52b5cac..000000000 --- a/docs/usage_docs/authoring_charts.md +++ /dev/null @@ -1 +0,0 @@ -#Coming Soon diff --git a/docs/usage_docs/getting-started-guide.md b/docs/usage_docs/getting-started-guide.md deleted file mode 100644 index 24f0b43a6..000000000 --- a/docs/usage_docs/getting-started-guide.md +++ /dev/null @@ -1,22 +0,0 @@ -#Under Construction - -#Getting Started Guide - -[Helm](https://helm.sh) helps you find and use software built for Kubernetes. With a few Helm commands you can quickly and easily deploy software packages like: - -- Postgres -- etcd -- HAProxy -- redis - -All of the Helm charts live at [github.com/kubernetes/charts](https://github.com/kubernetes/charts). If you want to make your own charts we have a guide for [authoring charts](authoring_charts.md). Charts should follow the defined [chart format](/docs/design/chart_format.md). - -Get started with the following steps: - -1. [Clone](github.com/kubernetes/helm) this project. - -2. Then, run `make build`. This will install all the binaries you need in `bin/` in the root of the directory. - -3. To see a list of helm commands, run `./bin/helm` - -4. `helm dm install` to install the server side component. diff --git a/docs/workflow/developer-workflows.md b/docs/workflow/developer-workflows.md deleted file mode 100644 index 015237b28..000000000 --- a/docs/workflow/developer-workflows.md +++ /dev/null @@ -1,540 +0,0 @@ -# Helm/DM Developer Experience Workflows - -This document outlines the individual workflows that Helm is designed to -solve. - -In this document we examine several workflows that we feel are central to -the experience of deploying, managing, and building applications using -Helm. Each workflow is followed by a basic model of where the processing -of commands occurs. - -### User Workflow - -The user workflow is the common case. This answers stories for the "tire -kicker" and "standard user" personas. - -#### Installation - -Currently, the client can be used with no installation. However, installing the server side component is done like this: - -``` -$ helm server install -``` - -- Client uses existing `kubectl` configuration to install built-in manifests. - -General pattern: -``` -helm server install -``` - -#### Searching - -*Not implemented yet* - -``` -$ helm search bar -helm:example.com/foo/bar - A basic chart -helm:example.com/foo/barracuda - A fishy chart -helm:example.com/foo/barbecue - A smoky chart -``` - -- Client submits the query to the manager -- Server searches the available chart repos - -General pattern: -``` -helm search PATTERN -``` - - -#### Simple deployment: - -``` -$ helm deploy -n wonky-panda helm:example.com/foo/bar -Created wonky-panda -``` - -- The client sends the server a request to deploy `helm:example.com/foo/bar`. -- The server assigns a random name `wonky-panda`, fetches the chart from - object storage, and goes about the deployment process. - -General patterns: -``` -helm deploy [-f CONFIG] [-n NAME] [CHART] -``` - - -#### Find out about params: - -*Not implemented yet* - -In this operation, helm reads a chart and returns the list of parameters -that can be supplied in a template: - -``` -$ helm chart show helm:example.com/foo/bar -Params: -- bgcolor: The background color for the home page (hex code or HTML colors) -- title: The title of the app -``` - -- The client sends the request to the API server -- The API server fetches the chart, analyzes it, and returns the list of - parameters. - -General pattern: -``` -helm chart show CHART -``` - -*Not implemented yet* - -#### Generate the params for me: - -In this operation, helm generates a parameter values file for the user. - -``` -$ helm chart params helm:example.com/foo/bar -Created: values.yaml -$ edit values.yaml -``` - -- The client sends the request to the server -- The server returns a stub file -- The client writes the file to disk - -General pattern: -``` -helm chart params CHART [CHART...] -``` - -#### Deploy with params: - -In this operation, the user deploys a chart with an associated values -file. - -``` -$ helm deploy -f values.yaml -n taco-tuesday helm:example.com/foo/bar -Created taco-tuesday -``` - -- The client sends a request with the name of the chart and the values - file. -- The API server generates a name (`taco-tuesday`) -- The API server fetches the request chart, sends the chart and values - through the template resolution phase, and deploys the results. - -If we allow the user to pass in a name (overriding the generated name), -then the server must first guarantee that this name is unique. - -Alternately, just the values file may be specified, since it contains a reference to the base chart. - -``` -$ helm deploy -f values.yaml -Created taco-wednesday -``` - -#### Get the info about named deployment. - -A deployment, as we have seen, is a named instance of a chart. -Operations that operate on these instances use the name to refer to the -instance. - -``` -$ helm status taco-tuesday -OK -Located at: 10.10.10.77:8080 -``` - -- The client sends the API server a request for the status of - `taco-tuesday`. -- The API server looks up pertinent data and returns it. -- The client displays the data. - -General pattern: -``` -helm status NAME -``` - -#### Edit and redeploy: - -*Not implemented yet* - -Redeployment is taking an existing _instance_ and changing its template -values, and then re-deploying it. - -``` -$ edit values.yaml -$ helm redeploy -f values.yaml taco-tuesday -Redeployed taco-tuesday -``` - -- The client sends the instance name and the new values to the server -- The server coalesces the values into the existing instance and then - restarts. - -General pattern: -``` -helm redeploy [-f CONFIG] NAME -``` - - -#### Delete: - -``` -$ helm deployment delete taco-tuesday -Destroyed taco-tuesday -``` - -- The client sends the DELETE instance name command -- The API server destroys the resource - -General pattern: -``` -helm deployment delete NAME [NAME...] -``` - -### Power User Features - -Users familiar with the system may desire additional tools. - -#### Name a deploy - -``` -$ helm deploy -name skinny-pigeon example.com/foo/bar -``` - -This follows the deployment process above. The server _must_ ensure that -the name is unique. - -#### Get values for an app: - -*Not implemented yet* - -``` -$ helm deployment params taco-tuesday -Stored in values.yaml -$ helm redeploy taco-tuesday values.yaml -``` - -- The client sends the instance name to the values endpoint -- The server returns the values file used to generate the instance. -- The client writes this to a file (or, perhaps, to STDOUT) - -General pattern: -``` -helm deployment params NAME [NAME...] -``` - -When more than one name is specified, the resulting file will contain configs for all names. - -#### Get fully generated manifest files - -*Not implemented yet* - -``` -$ helm deployment manifest taco-tuesday -Created manifest.yaml -``` - -- The client sends the instance name to the manifests endpoint -- The server returns the manifests, as generated during the - deploy/redeploy cycle done prior. - -General pattern: -``` -helm deployment manifest NAME [NAME...] -``` - -#### Auto-detecting helm problems - -``` -$ helm doctor -``` - -- The client performs local diagnostics and diagnostics of Kubernetes and the DM server. - -General pattern: -``` -helm doctor -``` - -#### Listing all installed charts - -*Not implemented yet* - -``` -$ helm chart list -helm:example.com/foo/bar#1.1.1 -helm:example.com/foo/bar#1.1.2 -helm:example.com/foo/barbecue#0.1.0 -``` - -- The client sends the server a request for all charts installed -- The server computes and responds - -General pattern: -``` -helm chart list -``` - -#### Get instances of a chart - -NB: We might rename this `helm chart instances`, as that is less vague. - -``` -$ helm chart get helm:example.com/foo/bar -taco-tuesday -taco-wednesday -``` - -- The client sends a request to the API server. -- The server responds with a list of deployment instance names. - -This retrieves a shallow list, and does not inspect instances for ancestor charts. - -General pattern: -``` -helm chart get CHART -``` - -### Listing deployments - -``` -$ helm deployment list -skinny-pigeon -taco-tuesday -taco-wednesday -``` - -- The client requests a list from the server -- The server returns the list - -General pattern: -``` -helm deployment list -``` - -### Getting details of a deployment - -*Not implemented yet* - -_NB: Might not need this._ - -``` -$ helm deployment show skinny-pigeon -DETAILS -``` - -### Developer Workflow - -This section covers the experience of the chart developer. - -In this case, when the client detects that it is working with a local -chart, it bundles the chart, and sends the entire chart, not just the -values. - -``` -$ helm chart create mychart -Created mychart/Chart.yaml -$ helm lint mychart -OK -$ helm deploy . -Uploading ./mychart as localchart/mychart-0.0.1.tgz -Created skinny-pigeon -$ helm status skinny-pigeon -OK -$ edit something -$ helm redeploy skinny-pigeon -Redeployed skinny-pigeon -``` - -- `helm chart create` and `helm lint` are client side operations -- `helm deploy`, `helm status`, and `helm redeploy` are explained above. - -General pattern for create: -``` -helm chart create [--from NAME] CHARTNAME -``` - -Where `NAME` will result in fetching the generated values from the cluster. - -General pattern for lint: -``` -helm lint PATH -``` -*Lint is not implemented yet* - - -#### Packaging and Releasing packages - -Package a chart: - -``` -$ helm chart package PATH/TO/FOO -Created foo-1.1.2.tgz -``` - -Releasing a chart: - -*Not implemented yet* - -``` -$ helm release -u https://example.com/bucket ./foo-1.1.2.tgz -Uploaded to https://example.com/bucket/foo-1.1.2.tgz -``` - -General pattern: -``` -helm chart package PATH -helm release [-u destination] PATH|FILE -``` - - -### Helm Cluster Management Commands - -#### Install a Helm server -``` -$ helm server install -``` -- Client installs using the current kubectl configuration - -General pattern: -``` -helm server [--dry-run] install -``` - -#### Uninstall - -``` -$ helm server uninstall -``` - -- The client interacts with the Kubernetes API server - -General pattern: -``` -helm server [--dry-run] uninstall -``` - -#### Check which cluster is the current target for helm - -``` -$ helm server target -API Endpoint: https://10.21.21.21/ -``` - -- The client interacts with the local Kubernetes config and the Kubernetes API server - -General pattern: -``` -helm server target -``` - -#### View status of DM service - -``` -$ helm server status -OK -``` - -- The client interacts with the Kubernetes API server - -General pattern: -``` -helm server status -``` - -### Repository Configuration - -#### Listing repositories - -``` -$ helm repo list -``` - -- The client request info from the server -- The server provides information about which repositories it is aware of - -General pattern: -``` -helm repo list -``` - -#### Adding credentials - -*Not implemented yet* - -``` -$ helm credential add aff34... 89897a... -Created token-foo -``` - -- The client sends a request to the server -- The server creates a new token and returns a name - -General pattern: -``` -helm credential add TOKEN SECRET -``` - -#### Adding a repository with credentials - -``` -$ helm repo add -c token-foo https://example.com/charts -``` - -The URL is of the form `PROTO://HOST[:PORT]/BUCKET`. - -General pattern: -``` -helm repo add [-c TOKEN_NAME] REPO_URL -``` - -#### Removing repositories - -``` -$ helm repo rm https://example.com/charts -``` - -- The client sends a request to the server -- The server removes the repo from the known list - -General pattern: -``` -helm repo rm REPO_URL -``` - -#### Listing Credentials - -*Not implemented yet* - -``` -$ helm credential list -token-foo: TOKEN -``` - -- The client requests a list of tokens -- The server returns the name and the token, but not the secret - -General pattern: -``` -helm credential list [PATTERN] -``` - -#### Removing credentials - -*Not implemented yet* - -``` -$ helm credential rm token-foo -``` - -- The client sends a request to the server -- The server deletes the credential - -General pattern: -``` -helm credential rm CREDENTIAL_NAME -``` diff --git a/docs/workflow/helm-dm-diagrams.src.md b/docs/workflow/helm-dm-diagrams.src.md deleted file mode 100644 index 03c3ecb9a..000000000 --- a/docs/workflow/helm-dm-diagrams.src.md +++ /dev/null @@ -1,84 +0,0 @@ -# Helm-DM Workflow Diagrams - -# Helm Official -@startuml helm-official-workflow.png -autonumber -actor Member -actor Core -participant GitHub -participant CI -database Repository -Member->GitHub: PR New Chart -GitHub->CI: Run automated tests -CI->GitHub: Tests pass -GitHub-->Member: Tests pass -GitHub-->Core: Tests pass -Core<->Member: Discussion about chart -Core->GitHub: Code review -Core->GitHub: Release approval -GitHub->CI: Issue release -CI->Repository: Push release -Member->Repository: Get release -@enduml - -# Public Chart Repository -An example workflow using Launchpad and BZR. -@startuml public-chart-repo.png -autonumber -actor Developer -actor Maintainer -participant Launchpad -Developer->Launchpad: Push branch with chart -Maintainer->Launchpad: Fetch branch -Maintainer->Maintainer: Locally test -Maintainer->Launchpad: Merge branch -... later ... -Maintainer->Launchpad: Create global release -Maintainer->Maintainer: Regenerate all versioned charts -Maintainer->Repository: Push charts -@enduml - -# Private chart repository -An example workflow using an internal Gerrit Git repo and Jenkins. -@startuml private-chart-repo.png -autonumber -actor "Developer A" as A -actor "Developer B" as B -actor "Internal user" as Internal -participant Gerrit -participant Jenkins -participant "Docker registry" as Docker -participant "Chart repository" as Chart -A->Gerrit: Clone repo -A->A: Local development -A->Gerrit: Push branch -Gerrit->Jenkins: Test branch -Jenkins->Jenkins: Build and test -Jenkins->Docker: Snapshot image push -Jenkins->Chart: Snapshot chart -Jenkins->Jenkins: Integration tests -Jenkins-->A: Build OK -B->Gerrit: Code review -B->A: Code approval -A->Gerrit: Tag the new chart -Gerrit->Jenkins: Release -Jenkins->Jenkins: Build and test -Jenkins->Docker: Production image push -Jenkins->Chart: Production chart -Internal->Chart: Get production chart -@enduml - -# Private Charts without repository -An example using SVN for local dev, and no chart repository. -@startuml private-chart-no-repo.png -autonumber -actor "Developer A" as A -actor "Developer B" as B -participant SVN -participant Kubernetes -A->SVN: Checkout code -A->A: Develop locally -A->SVN: Check in code -B<->SVN: Update local -B->Kubernetes: Run helm deploy on local chart -@enduml diff --git a/docs/workflow/helm-official-workflow.png b/docs/workflow/helm-official-workflow.png deleted file mode 100644 index 049c9f68b30bdca46729752a66657e07a4088e91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30322 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 diff --git a/docs/workflow/private-chart-no-repo.png b/docs/workflow/private-chart-no-repo.png deleted file mode 100644 index 4919d63bc701e807e76154b778dd86af6beff67b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18297 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 WQ%2sM2cOirB_pXQQ6^>>@c#fg70wL+ diff --git a/docs/workflow/private-chart-repo.png b/docs/workflow/private-chart-repo.png deleted file mode 100644 index bf9d17e8927bc56fe2649dc351f0c65e3cc0f0c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45502 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 K{Ke@DfBt{=^%Kqj diff --git a/docs/workflow/public-chart-repo.png b/docs/workflow/public-chart-repo.png deleted file mode 100644 index 31c9f73a86166b93163b5cb19a0d542283777747..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22859 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 diff --git a/docs/workflow/team-workflows.md b/docs/workflow/team-workflows.md deleted file mode 100644 index fb55d12bf..000000000 --- a/docs/workflow/team-workflows.md +++ /dev/null @@ -1,226 +0,0 @@ -# Team Workflows - -This document explains the four team workflows that Helm is designed to -solve. - -## An Overview of Four Team Workflows - -The remainder of this document is broken into two major sections: The Overview covers the -broad characteristics of the four workflows. The Development Cycle -section covers how the developer cycle plays out for each of the four -workflows. - -This section provides an introduction to the four workflows outlined -above, calling out characteristics of each workflow that define it -against other workflows. - -### Helm Official - -The _Helm Official_ project focuses on maintaining a repository of high-quality production-ready charts. Charts may be contributed by anyone in the broad community, and they are vetted and maintained by the charts-maintainers team in the Github Kubernetes organization. - -Stage | Devel | Review | Release | Store | Use -------|-------|--------|---------|-------|----- -Operations | Create/modify chart | Code review | Sign, version, package | Store releases | Get, Use -Who? | Developer | Us | Us | Us | Anyone -How? | Dev PR | We review | We release and sign | We upload to managed storage | Chart is public - -Characteristics of the Helm Official workflow: - -- Source code for charts is maintained in a GitHub repository -- Chart source is linted and tested by automated tools and human beings -- Charts are released by core contributors to the project -- All Charts are made available under the Apache 2 license -- When a chart is released, an official binary distribution is uploaded to the official chart repository in Google Cloud Storage. In addition, Helm Official charts have the following characteristics: - - They released by community members - - They are versioned independently (each chart has its own version) - - They are signed/provenanced by one of the official Helm GPG keys - - They are made available with no authentication or registration requirements - -### Public Unofficial - -The Helm project need not be the only repository available to users. Other organizations may wish to host their own repositories, and load whatever charts they so choose. - -We attempt to provide flexibility that allows such organizations to build repositories as they see fit, but keep the end user's experience roughly similar. - -Stage | Devel | Review | Release | Store | Use -------|-------|--------|---------|-------|----- -Operations | Create/modify chart | Code review | Sign, version, package | Store releases | Get, Use -Who? | Developer | Org? | Org? | Org | Anyone -How? | Dev Contribution | Org defines this | Org defines this | Org hosts | Chart is public - - -A public unofficial chart repository is free to structure the development cycle as they see fit. - -- We have no opinions about the source control (if any) used for chart development -- We provide `helm release` as a tool for releasing a chart - - We strongly advise public repository maintainers to use provenance files - - We strongly advise public repository maintainers to make public keys available to the general public. -- Charts must be stored in one of the supported object storage repositories, including Google Cloud Storage and S3 compatible storage -- Helm/DM provides the following tools for working with such repositories: - - `helm release` takes local source and builds a chart. - - `helm repo add|list|rm` allows users to add, list, and remove repositories from the recognized list. - - `helm repo push` allows new charts to be pushed to a remote server - - -### Private Chart Repositories - -We anticipate that some Helm users will desire to have private repositories over which they have control over both the development cycle and the availability of the released charts. - -Similar to our approach to unofficial chart repositories, we attempt to provide a tool general enough that it can be used for private chart repositories. - -Stage | Devel | Review | Release | Store | Use -------|-------|--------|---------|-------|----- -Operations | Create/modify chart | Code review | Sign, version, package | Store releases | Get, Use -Who? | Org | Org | Org | Org | Org -How? | Org defines this | Org defines this | Org defines this | Org hosts | Org controls access - - -We assume the following about this workflow: - -- We have no opinions about the source control (if any) used for chart development -- We provide `helm release` as a tool for releasing a chart - - While we suggest signing charts, this is a discretionary exercise for private repositories -- Charts may be stored in private GCS, S3, or compatible object storage. Helm is tested on Minio (an S3 compatible server for private hosting) to ensure a viable private cloud mechanism. - - Private repositories may require authentication via token/secret, as well as access controls. -- Helm/DM provides the following tools for working with such repositories: - - `helm release` takes local source and builds a chart. - - `helm repo add|list|rm` allows users to add, list, and remove repositories from the recognized list. - - `helm repo push` allows new charts to be pushed to a remote server - - -### Private Charts without Repositories - -In some cases, an organization may wish to create charts, but not store them in a chart repository. While this is not the preferred Helm workflow, we support tooling for this method. - -Stage | Devel | Review | Release | Store | Use -------|-------|--------|---------|-------|----- -Operations | Create/modify chart | Code review | Sign, version, package | Store releases | Get, Use -Who? | Org | Org | Org | N/A | Org -How? | Org defines this | Org defines this | Org defines this | N/A | Org defines this - -We operate on a different set of assumptions for this workflow. - -- We have no opinions about the source code control (if any) used. -- We have no opinions about access control when repositories are not involved. -- Charts must be pushed into the cluster (the cluster will not pull from a non-repository location) -- Helm/DM provides the following tools for this workflow: - - `helm release` builds a package - - `helm deploy` pushes a package into a cluster - -## Development Cycles for Each Workflow - -In this section, we explain the developer cycle for each of the four workflows above. - -### 0. Pre-Submission Workflow (aka Local Development) - -Prior to submitting a chart for release, developers may use the following workflow. This workflow is assumed in all four sections below. - -1. Create a chart with `helm create MYCHART` (or manually) -2. Edit the chart -3. Test the chart's standards conformance with `helm lint MYCHART` -4. Run a test deployment using `helm deploy MYCHART` - -### 1. The Helm Official Development Cycle - -The Helm Official project maintains source code in a location that is readily available to all of the community. Source code is stored on GitHub in the official (`github.com/helm/charts` or `github.com/kubernetes/charts`) repository. GitHub facilitates a development workflow that this project uses for chart maintenance. - -There are two general classes of user that are important to this workflow: - -- Core Contributors: Core contributors are developers that have been given special stewardship responsibilities over the repository. They have the following responsibilities: - - Review submissions to the repository - - Approve charts for release - - Respond to issues with existing charts -- Community Members: Any user of the Helm Official project who is not a core contributor. - - - -1. PULL REQUEST: A new chart (or an update to an existing chart) is contributed by a core maintainer or community member. This is done using GitHub pull requests. -2. AUTOMATED TESTING: Automated testing tools evaluate the contribution for the following: - - CLA approval of the submitter - - Style/format adherence - - Unit, functional, and/or integration tests pass -3. DISCUSSION: Any discussion on the pull request may happen using GitHub's commentary features -3. CODE REVIEW: Two or more core contributors must review the code and sign off on it. -4. RELEASE APPROVAL: If a chart is approved for release, a core maintainer may mark it as such -5. AUTOMATED RELEASE: Once a chart is approved for release, an automated tool will bundle the chart, sign it using an official signature, and upload it into the Helm Official repository - -### 2. Unofficial Public Repositories - -Unofficial repositories do not have a well-defined development workflow, but have a semi-rigid release workflow. - -- The public repository must use a supported object storage system -- Charts must be in the same format - -#### A Hypothetical Dev Workflow - -An organization has a Bazaar (bzr) project maintained at _launchpad.net_, and has no automated tooling around the project. - - - -1. Developers work off of copies of the Bazaar code base -2. Developers push branches into the code review system when ready -2. A central maintainer approves and merges reviewed code -3. At a fixed point in time, the project administrator releases a new version of the entire repository -4. During this process, all charts are version, packaged/signed, and uploaded to the project's S3 repository, where they are made available to users - -The above workflow illustrates how a development team may conduct public shared development, and release to a public repository, but with a workflow that diverges substantially from the model of the Helm Official repository. - -### 3. Private Chart Repository - -In this model, the entire process is managed by an organization. - -#### A Hypothetical Dev Workflow - -The organization uses an internally hosted Git server, Gerrit for code review, and Jenkins for automation. They host an internal repository on Minio. This repository has a combination of pre-approved Helm Official packages (copied from upstream) and internal packages. - -In this workflow, charts are not stored together. Instead, each chart is stored alongside the Docker image source code. For example, the repository named `corpcalendar.git` is laid out as follows: - -``` -corpcalendar/ - Dockerfile - src/ - ... # corpcalendar source code - chart/ - corpcalendar - Chart.yaml - ... # Helm Chart files -``` - -Each separate project is structured in this way. - - - -1. Developers clone a desired repo (`git clone .../corpcalendar.git`) -2. Developers work on the code and chart together -3. Upon each commit, Jenkins does the following: - - Project tests are run - - A snapshot Docker image is built and stored in a snapshot Docker registry - - A snapshot chart is built and stored on a snapshot Helm repository - - Integration tests are run -4. When the developer is ready for a release, she tags the repository (`git tag v1.2.3`) and pushes the tag -5. Upon a tag commit, Jenkins does the following: - - Runs tests - - Creates a final Docker image and uploads to the internal release Docker registry - - Creates a final chart and uploads to the internal Helm repository -6. Internal users may then access the chart at the internal Helm repository - -This workflow represents a method common in enterprises, and also illustrates how chart development need not occur in one aggregated repository. - -### 4. Private Charts without Repositories - -This model is used in cases where an organization chooses not to host a Chart repository. While we don't advise using this method, a workflow exists. - -While we don't offer an opinionated VCS workflow, we also suggest that one is used. In our example below, we draw on VCS usage. - -#### A Hypothetical Dev Workflow - -A small development team uses Subversion (SVN) to host their internal projects. They do not use a chart repository, nor do they employ any automated testing tools outside of Helm. - - - -1. Developer Andy checks out a copy of the SVN repository (`svn co ...`) -2. Developer Andy edits charts locally, and test locally -3. When a chart is ready for sharing, the developer checks in the revised chart (`svn ci ...`) -4. Developer Barb updates her local copy (`svn up`) -5. Developer Barb then uses `helm deploy ./localchart` to deploy this chart into production - diff --git a/examples/charts/nginx/Chart.yaml b/examples/charts/nginx/Chart.yaml deleted file mode 100644 index f9c0e4460..000000000 --- a/examples/charts/nginx/Chart.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: nginx -description: nginx http service -version: 0.0.1 -keywords: -- web server -- http -- https -- proxy -maintainers: -- name: Melinda Doe - email: Melinda@helm.com -home: https://www.nginx.com diff --git a/examples/charts/nginx/LICENSE b/examples/charts/nginx/LICENSE deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/charts/nginx/README.md b/examples/charts/nginx/README.md deleted file mode 100644 index 1495f5242..000000000 --- a/examples/charts/nginx/README.md +++ /dev/null @@ -1 +0,0 @@ -This is an example of an nginx chart. diff --git a/examples/charts/nginx/templates/nginx-rc.yaml b/examples/charts/nginx/templates/nginx-rc.yaml deleted file mode 100644 index a0dfdf935..000000000 --- a/examples/charts/nginx/templates/nginx-rc.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: nginx -spec: - replicas: 1 - selector: - name: nginx - template: - metadata: - labels: - name: nginx - spec: - containers: - - name: nginx - image: nginx - ports: - - containerPort: 80 diff --git a/examples/charts/nginx/templates/nginx-svc.yaml b/examples/charts/nginx/templates/nginx-svc.yaml deleted file mode 100644 index e6c450d9a..000000000 --- a/examples/charts/nginx/templates/nginx-svc.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: nginx - labels: - app: nginx -spec: - ports: - - port: 80 - protocol: TCP - name: http - selector: - name: nginx diff --git a/examples/charts/redis/Chart.yaml b/examples/charts/redis/Chart.yaml deleted file mode 100644 index 466d19e2e..000000000 --- a/examples/charts/redis/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: redis -description: Port of the replicatedservice template from kubernetes/charts -version: "2.0.0" -home: "" -expander: - name: expandybird-service - entrypoint: templates/redis.jinja diff --git a/examples/charts/redis/templates/redis.jinja b/examples/charts/redis/templates/redis.jinja deleted file mode 100644 index 46cd615c3..000000000 --- a/examples/charts/redis/templates/redis.jinja +++ /dev/null @@ -1,32 +0,0 @@ -{% set REDIS_PORT = 6379 %} -{% set WORKERS = properties['workers'] if properties and properties['workers'] else 2 %} - -resources: -- name: redis-master - type: gs://kubernetes-charts-testing/replicatedservice-3.tgz - properties: - # This has to be overwritten since service names are hard coded in the code - service_name: redis-master - service_port: {{ REDIS_PORT }} - target_port: {{ REDIS_PORT }} - container_port: {{ REDIS_PORT }} - replicas: 1 - container_name: master - image: redis - -- name: redis-slave - type: gs://kubernetes-charts-testing/replicatedservice-3.tgz - properties: - # This has to be overwritten since service names are hard coded in the code - service_name: redis-slave - service_port: {{ REDIS_PORT }} - container_port: {{ REDIS_PORT }} - replicas: {{ WORKERS }} - container_name: worker - image: kubernetes/redis-slave:v2 - # An example of how to specify env variables. - env: - - name: GET_HOSTS_FROM - value: env - - name: REDIS_MASTER_SERVICE_HOST - value: redis-master diff --git a/examples/charts/redis/templates/redis.jinja.schema b/examples/charts/redis/templates/redis.jinja.schema deleted file mode 100644 index cd550d65a..000000000 --- a/examples/charts/redis/templates/redis.jinja.schema +++ /dev/null @@ -1,10 +0,0 @@ -info: - title: Redis cluster - description: Defines a redis cluster, using a single replica - replicatedservice for master and replicatedservice for workers. - -properties: - workers: - type: int - default: 2 - description: Number of worker replicas. diff --git a/examples/charts/replicatedservice/Chart.yaml b/examples/charts/replicatedservice/Chart.yaml deleted file mode 100644 index 5f885da24..000000000 --- a/examples/charts/replicatedservice/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: replicatedservice -description: Port of the replicatedservice template from kubernetes/charts -version: "3.0.0" -home: "" -expander: - name: expandybird-service - entrypoint: templates/replicatedservice.py diff --git a/examples/charts/replicatedservice/templates/replicatedservice.py b/examples/charts/replicatedservice/templates/replicatedservice.py deleted file mode 100644 index 73fc6203d..000000000 --- a/examples/charts/replicatedservice/templates/replicatedservice.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Defines a ReplicatedService type by creating both a Service and an RC. - -This module creates a typical abstraction for running a service in a -Kubernetes cluster, namely a replication controller and a service packaged -together into a single unit. -""" - -import yaml - -SERVICE_TYPE_COLLECTION = 'Service' -RC_TYPE_COLLECTION = 'ReplicationController' - - -def GenerateConfig(context): - """Generates a Replication Controller and a matching Service. - - Args: - context: Template context. See schema for context properties - - Returns: - A Container Manifest as a YAML string. - """ - # YAML config that we're going to create for both RC & Service - config = {'resources': []} - - name = context.env['name'] - container_name = context.properties.get('container_name', name) - namespace = context.properties.get('namespace', 'default') - - # Define things that the Service cares about - service_name = context.properties.get('service_name', name + '-service') - service_type = SERVICE_TYPE_COLLECTION - - # Define things that the Replication Controller (rc) cares about - rc_name = context.properties.get('rc_name', name + '-rc') - rc_type = RC_TYPE_COLLECTION - - service = { - 'name': service_name, - 'properties': { - 'apiVersion': 'v1', - 'kind': 'Service', - 'metadata': { - 'labels': GenerateLabels(context, service_name), - 'name': service_name, - 'namespace': namespace, - }, - 'spec': { - 'ports': [GenerateServicePorts(context, container_name)], - 'selector': GenerateLabels(context, name) - } - }, - 'type': service_type, - } - set_up_external_lb = context.properties.get('external_service', None) - if set_up_external_lb: - service['properties']['spec']['type'] = 'LoadBalancer' - cluster_ip = context.properties.get('cluster_ip', None) - if cluster_ip: - service['properties']['spec']['clusterIP'] = cluster_ip - - rc = { - 'name': rc_name, - 'type': rc_type, - 'properties': { - 'apiVersion': 'v1', - 'kind': 'ReplicationController', - 'metadata': { - 'labels': GenerateLabels(context, rc_name), - 'name': rc_name, - 'namespace': namespace, - }, - 'spec': { - 'replicas': context.properties['replicas'], - 'selector': GenerateLabels(context, name), - 'template': { - 'metadata': { - 'labels': GenerateLabels(context, name), - }, - 'spec': { - 'containers': [ - { - 'env': GenerateEnv(context), - 'image': context.properties['image'], - 'name': container_name, - 'ports': [ - { - 'name': container_name, - 'containerPort': context.properties['container_port'], - } - ], - } - ], - } - } - } - } - } - - # Set up volume mounts - if context.properties.get('volumes', None): - rc['properties']['spec']['template']['spec']['containers'][0]['volumeMounts'] = [] - rc['properties']['spec']['template']['spec']['volumes'] = [] - for volume in context.properties['volumes']: - # mountPath should be unique - volume_name = volume['mount_path'].replace('/', '-').lstrip('-') + '-storage' - rc['properties']['spec']['template']['spec']['containers'][0]['volumeMounts'].append( - { - 'name': volume_name, - 'mountPath': volume['mount_path'] - } - ) - del volume['mount_path'] - volume['name'] = volume_name - rc['properties']['spec']['template']['spec']['volumes'].append(volume) - - if context.properties.get('privileged', False): - rc['properties']['spec']['template']['spec']['containers'][0]['securityContext'] = { - 'privileged': True - } - - config['resources'].append(rc) - config['resources'].append(service) - return yaml.dump(config) - - -# Generates labels either from the context.properties['labels'] or generates -# a default label 'name':name -def GenerateLabels(context, name): - """Generates labels from context.properties['labels'] or creates default. - - We make a deep copy of the context.properties['labels'] section to avoid - linking in the yaml document, which I believe reduces readability of the - expanded template. If no labels are given, generate a default 'name':name. - - Args: - context: Template context, which can contain the following properties: - labels - Labels to generate - - Returns: - A dict containing labels in a name:value format - """ - tmp_labels = context.properties.get('labels', None) - ret_labels = {'name': name} - if isinstance(tmp_labels, dict): - for key, value in tmp_labels.iteritems(): - ret_labels[key] = value - return ret_labels - - -def GenerateServicePorts(context, name): - """Generates a ports section for a service. - - Args: - context: Template context, which can contain the following properties: - service_port - Port to use for the service - target_port - Target port for the service - protocol - Protocol to use. - - Returns: - A dict containing a port definition - """ - container_port = context.properties['container_port'] - target_port = context.properties.get('target_port', container_port) - service_port = context.properties.get('service_port', target_port) - protocol = context.properties.get('protocol') - - ports = {} - if name: - ports['name'] = name - if service_port: - ports['port'] = service_port - if target_port: - ports['targetPort'] = target_port - if protocol: - ports['protocol'] = protocol - - return ports - -def GenerateEnv(context): - """Generates environmental variables for a pod. - - Args: - context: Template context, which can contain the following properties: - env - Environment variables to set. - - Returns: - A list containing env variables in dict format {name: 'name', value: 'value'} - """ - env = [] - tmp_env = context.properties.get('env', []) - for entry in tmp_env: - if isinstance(entry, dict): - env.append({'name': entry.get('name'), 'value': entry.get('value')}) - return env diff --git a/examples/charts/replicatedservice/templates/replicatedservice.py.schema b/examples/charts/replicatedservice/templates/replicatedservice.py.schema deleted file mode 100644 index 712ffd315..000000000 --- a/examples/charts/replicatedservice/templates/replicatedservice.py.schema +++ /dev/null @@ -1,91 +0,0 @@ -info: - title: Replicated Service - description: | - Defines a ReplicatedService type by creating both a Service and an RC. - - This module creates a typical abstraction for running a service in a - Kubernetes cluster, namely a replication controller and a service packaged - together into a single unit. - -required: -- image - -properties: - container_name: - type: string - description: Name to use for container. If omitted, name is used. - service_name: - type: string - description: Name to use for service. If omitted, name-service is used. - namespace: - type: string - description: Namespace to create resources in. If omitted, 'default' is - used. - default: default - protocol: - type: string - description: Protocol to use for the service. - service_port: - type: int - description: Port to use for the service. - target_port: - type: int - description: Target port to use for the service. - container_port: - type: int - description: Port to use for the container. - replicas: - type: int - description: Number of replicas to create in RC. - image: - type: string - description: Docker image to use for replicas. - labels: - type: object - description: Labels to apply. - env: - type: array - description: Environment variables to apply. - properties: - name: - type: string - value: - type: string - external_service: - type: boolean - description: If set to true, enable external load balancer. - cluster_ip: - type: string - description: IP to use for the service - privileged: - type: boolean - description: If set to true, enable privileged container - volumes: - type: array - description: Volumes to mount. - items: - type: object - properties: - mounth_path: - type: string - description: Path to mount volume - # See https://cloud.google.com/container-engine/docs/spec-schema?hl=en for possible volumes. Since we only use gcePersistentDisk and NFS in our examples we have only added these ones. - oneOf: - gcePersistentDisk: - pdName: - type: string - description: Persistent's disk name - fsType: - type: string - description: Filesystem type of the persistent disk - nfs: - server: - type: string - description: The hostname or IP address of the NFS server - path: - type: string - description: The path that is exported by the NFS server - readOnly: - type: boolean - description: Forces the NFS export to be mounted with read-only permissions - diff --git a/examples/guestbook/README.md b/examples/guestbook/README.md deleted file mode 100644 index b64795a5a..000000000 --- a/examples/guestbook/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Guestbook Example - -Welcome to the Guestbook example. It shows you how to build and reuse -parameterized templates. - -## Prerequisites - -First, make sure DM is installed in your Kubernetes cluster and that the -Guestbook example is deployed by following the instructions in the top level -[README.md](../../README.md). - -## Understanding the Guestbook example - -Let's take a closer look at the configuration used by the Guestbook example. - -### Replicated services - -The typical design pattern for microservices in Kubernetes is to create a -replication controller and a service with the same selector, so that the service -exposes ports from the pods managed by the replication controller. - -We have created a parameterized template for this kind of replicated service -called [Replicated Service](https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v1), and we use it -three times in the Guestbook example. - -The template is defined by a -[Python script](https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v1/replicatedservice.py). It -also has a [schema](https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v1/replicatedservice.py.schema). -Schemas are optional. If provided, they are used to validate template invocations -that appear in configurations. - -For more information about templates and schemas, see the -[design document](../../docs/design/design.md#templates). - -### The Guestbook application -The Guestbook application consists of 2 microservices: a front end and a Redis -cluster. - -#### The front end - -The front end is a replicated service with 3 replicas: - -``` -- name: frontend - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/common/replicatedservice/v1/replicatedservice.py - properties: - service_port: 80 - container_port: 80 - external_service: true - replicas: 3 - image: gcr.io/google_containers/example-guestbook-php-redis:v3 -``` - -(Note that we use the URL for a specific version of the template replicatedservice.py, -not just the template name.) - -#### The Redis cluster - -The Redis cluster consists of two replicated services: a master with a single replica -and the slaves with 2 replicas. It's defined by [this template](https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/storage/redis/v1/redis.jinja), -which is a [Jinja](http://jinja.pocoo.org/) file with a [schema](https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/storage/redis/v1/redis.jinja.schema). - -``` -{% set REDIS_PORT = 6379 %} -{% set WORKERS = properties['workers'] or 2 %} - -resources: -- name: redis-master - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v1/replicatedservice.py - properties: - # This has to be overwritten since service names are hard coded in the code - service_name: redis-master - service_port: {{ REDIS_PORT }} - target_port: {{ REDIS_PORT }} - container_port: {{ REDIS_PORT }} - replicas: 1 - container_name: master - image: redis - -- name: redis-slave - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v1/replicatedservice.py - properties: - # This has to be overwritten since service names are hard coded in the code - service_name: redis-slave - service_port: {{ REDIS_PORT }} - container_port: {{ REDIS_PORT }} - replicas: {{ WORKERS }} - container_name: worker - image: kubernetes/redis-slave:v2 - # An example of how to specify env variables. - env: - - name: GET_HOSTS_FROM - value: env - - name: REDIS_MASTER_SERVICE_HOST - value: redis-master -``` - -### Displaying types - -You can see both the both primitive types and the templates you've deployed to the -cluster using the `deployed-types` command: - -``` -dm deployed-types - -["Service","ReplicationController","redis.jinja","https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v1/replicatedservice.py"] -``` - -This output shows 2 primitive types (Service and ReplicationController), and 2 -templates (redis.jinja and one imported from github named replicatedservice.py). - -You can also see where a specific type is being used with the `deployed-instances` command: - -``` -dm deployed-instances Service -[{"name":"frontend-service","type":"Service","deployment":"guestbook4","manifest":"manifest-1446682551242763329","path":"$.resources[0].resources[0]"},{"name":"redis-master","type":"Service","deployment":"guestbook4","manifest":"manifest-1446682551242763329","path":"$.resources[1].resources[0].resources[0]"},{"name":"redis-slave","type":"Service","deployment":"guestbook4","manifest":"manifest-1446682551242763329","path":"$.resources[1].resources[1].resources[0]"}] -``` - -This output describes the deployment and manifest, as well as the JSON paths to -the instances of the type within the layout. - -For more information about deployments, manifests and layouts, see the -[design document](../../docs/design/design.md#api-model). - - diff --git a/examples/guestbook/guestbook.yaml b/examples/guestbook/guestbook.yaml deleted file mode 100644 index 9a31d2489..000000000 --- a/examples/guestbook/guestbook.yaml +++ /dev/null @@ -1,12 +0,0 @@ -resources: -- name: frontend - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v1 - properties: - service_port: 80 - container_port: 80 - external_service: true - replicas: 3 - image: gcr.io/google_containers/example-guestbook-php-redis:v3 -- name: redis - type: github.com/kubernetes/application-dm-templates/storage/redis:v1 - properties: null diff --git a/examples/package/cassandra.yaml b/examples/package/cassandra.yaml deleted file mode 100644 index 70c08b6f6..000000000 --- a/examples/package/cassandra.yaml +++ /dev/null @@ -1,4 +0,0 @@ -resources: -- name: cassandra - type: github.com/helm/charts/cassandra - properties: null diff --git a/examples/wordpress/README.md b/examples/wordpress/README.md deleted file mode 100644 index 8c976c73e..000000000 --- a/examples/wordpress/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# Wordpress Example -Welcome to the Wordpress example. It shows you how to deploy a Wordpress application using Deployment Manager. - -## Prerequisites -### Deployment Manager -First, make sure DM is installed in your Kubernetes cluster by following the instructions in the top level -[README.md](../../README.md). - -### Google Cloud Resources -The Wordpress application will make use of several persistent disks, which we will host on Google Cloud. To create these disks we will create a deployment using Google Cloud Deployment Manager: -```gcloud deployment-manager deployments create wordpress-resources --config wordpress-resources.yaml``` - -where `wordpress-resources.yaml` looks as follows: - -``` -resources: -- name: nfs-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 -- name: mysql-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 -``` - -### Privileged containers -To use NFS we need to be able to launch privileged containers. Since the release of Kubernetes 1.1 privileged container support is enabled by default. If your Kubernetes cluster doesn't support privileged containers you need to manually change this by setting the flag at `kubernetes/saltbase/pillar/privilege.sls` to true. - -### NFS Library -Mounting NFS volumes requires NFS libraries. Since the release of Kubernetes 1.1 the NFS libraries are installed by default. If they are not installed on your Kubernetes cluster you need to install them manually. - -## Understanding the Wordpress example template -Let's take a closer look at the template used by the Wordpress example. The Wordpress application consists of 4 microservices: an nginx service, a wordpress-php service, a MySQL service, and an NFS service. The architecture looks as follows: - - - -### Variables -The template contains the following variables: - -``` -{% set PROPERTIES = properties or {} %} -{% set PROJECT = PROPERTIES['project'] or 'kubernetes-charts' %} -{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %} -{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %} -{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %} -{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %} -{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %} -{% set NGINX = PROPERTIES['nginx'] or {} %} -{% set NGINX_PORT = 80 %} -{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %} -{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %} -{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %} -{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %} -{% set MYSQL = PROPERTIES['mysql'] or {} %} {% set MYSQL_PORT = MYSQL['port'] or 3306 %} {% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %} {% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %} {% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %} -``` - -### Nginx service -The nginx service is a replicated service with 2 replicas: - -``` -- name: nginx - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v2/replicatedservice.py - properties: - service_port: {{ NGINX_PORT }} - container_port: {{ NGINX_PORT }} - replicas: {{ NGINX_REPLICAS }} - external_service: true - image: gcr.io/{{ PROJECT }}/nginx:latest - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs -``` - -The nginx image builds upon the standard nginx image and simply copies a custom configuration file. - -### Wordpress-php service -The wordpress-php service is a replicated service with 2 replicas: - -``` -- name: wordpress-php - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v2/replicatedservice.py - properties: - service_name: wordpress-php - service_port: {{ WORDPRESS_PHP_PORT }} - container_port: {{ WORDPRESS_PHP_PORT }} - replicas: 2 - image: wordpress:fpm - env: - - name: WORDPRESS_DB_PASSWORD - value: {{ MYSQL_PASSWORD }} - - name: WORDPRESS_DB_HOST - value: mysql-service - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs -``` - -### MySQL service -The MySQL service is a replicated service with a single replica: - -``` -- name: mysql - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/common/replicatedservice/v2/replicatedservice.py - properties: - service_port: {{ MYSQL_PORT }} - container_port: {{ MYSQL_PORT }} - replicas: 1 - image: mysql:5.6 - env: - - name: MYSQL_ROOT_PASSWORD - value: {{ MYSQL_PASSWORD }} - volumes: - - mount_path: /var/lib/mysql - gcePersistentDisk: - pdName: {{ MYSQL_DISK }} - fsType: {{ MYSQL_DISK_FSTYPE }} -``` - -### NFS service -The NFS service is a replicated service with a single replica that is available as a type: - -``` -- name: nfs - type: https://raw.githubusercontent.com/kubernetes/application-dm-templates/master/storage/nfs/v1/nfs.jinja - properties: - ip: {{ NFS_SERVER_IP }} - port: {{ NFS_SERVER_PORT }} - disk: {{ NFS_SERVER_DISK }} - fstype: {{NFS_SERVER_DISK_FSTYPE }} -``` - -## Deploying Wordpress -We can now deploy Wordpress using: - -``` -dm deploy examples/wordpress/wordpress.yaml -``` - -where `wordpress.yaml` looks as follows: - -``` -imports: -- path: wordpress.jinja - -resources: -- name: wordpress - type: wordpress.jinja - properties: - project: <YOUR PROJECT> -``` diff --git a/examples/wordpress/architecture.png b/examples/wordpress/architecture.png deleted file mode 100644 index 853039e63312eaeebcbbe8cdb1c24f7c59c3f88e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33703 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 R@UtS2OS)Gs6rZ!d_g{5g+-?8> diff --git a/examples/wordpress/images/nginx/Dockerfile b/examples/wordpress/images/nginx/Dockerfile deleted file mode 100644 index ce14e868d..000000000 --- a/examples/wordpress/images/nginx/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM nginx -COPY default.conf /etc/nginx/conf.d/default.conf diff --git a/examples/wordpress/images/nginx/Makefile b/examples/wordpress/images/nginx/Makefile deleted file mode 100644 index 4933efb52..000000000 --- a/examples/wordpress/images/nginx/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -.PHONY: all build push clean - -DOCKER_REGISTRY = gcr.io -PREFIX = $(DOCKER_REGISTRY)/$(DOCKER_PROJECT) -IMAGE = nginx -TAG = latest - -DIR = . - -all: build - -build: - docker build -t $(PREFIX)/$(IMAGE):$(TAG) $(DIR) - -push: build -ifeq ($(DOCKER_REGISTRY),gcr.io) - gcloud docker push $(PREFIX)/$(IMAGE):$(TAG) -else - docker push $(PREFIX)/$(IMAGE):$(TAG) -endif - -clean: - docker rmi $(PREFIX)/$(IMAGE):$(TAG) diff --git a/examples/wordpress/images/nginx/default.conf b/examples/wordpress/images/nginx/default.conf deleted file mode 100644 index af2d7ebf8..000000000 --- a/examples/wordpress/images/nginx/default.conf +++ /dev/null @@ -1,48 +0,0 @@ -upstream phpcgi { - server wordpress-php:9000; -} - -server { - listen 80 ; - - root /var/www/html; - index index.php index.html index.htm; - - server_name localhost; - - location / { - try_files $uri $uri/ =404; - } - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini - - # With php5-cgi alone: - fastcgi_pass phpcgi; - - # With php5-fpm: - fastcgi_index index.php; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param REQUEST_URI $request_uri; - fastcgi_param DOCUMENT_URI $document_uri; - fastcgi_param DOCUMENT_ROOT $document_root; - fastcgi_param SERVER_PROTOCOL $server_protocol; - - fastcgi_param GATEWAY_INTERFACE CGI/1.1; - fastcgi_param SERVER_SOFTWARE nginx; - - fastcgi_param REMOTE_ADDR $remote_addr; - fastcgi_param REMOTE_PORT $remote_port; - fastcgi_param SERVER_ADDR $server_addr; - fastcgi_param SERVER_PORT $server_port; - fastcgi_param SERVER_NAME $server_name; - #include fastcgi_params; - } -} diff --git a/examples/wordpress/wordpress-resources.yaml b/examples/wordpress/wordpress-resources.yaml deleted file mode 100644 index 00f709de0..000000000 --- a/examples/wordpress/wordpress-resources.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Google Cloud Deployment Manager template -resources: -- name: nfs-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 -- name: mysql-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 diff --git a/examples/wordpress/wordpress.jinja b/examples/wordpress/wordpress.jinja deleted file mode 100644 index 0b84dacf4..000000000 --- a/examples/wordpress/wordpress.jinja +++ /dev/null @@ -1,71 +0,0 @@ -{% set PROPERTIES = properties or {} %} -{% set PROJECT = PROPERTIES['project'] or 'kubernetes-charts' %} -{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %} -{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %} -{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %} -{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %} -{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %} -{% set NGINX = PROPERTIES['nginx'] or {} %} -{% set NGINX_PORT = 80 %} -{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %} -{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %} -{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %} -{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %} -{% set MYSQL = PROPERTIES['mysql'] or {} %} -{% set MYSQL_PORT = MYSQL['port'] or 3306 %} -{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %} -{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %} -{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %} - -resources: -- name: nfs - type: github.com/kubernetes/application-dm-templates/storage/nfs:v1 - properties: - ip: {{ NFS_SERVER_IP }} - port: {{ NFS_SERVER_PORT }} - disk: {{ NFS_SERVER_DISK }} - fstype: {{NFS_SERVER_DISK_FSTYPE }} -- name: nginx - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_port: {{ NGINX_PORT }} - container_port: {{ NGINX_PORT }} - replicas: {{ NGINX_REPLICAS }} - external_service: true - image: gcr.io/{{ PROJECT }}/nginx:latest - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs -- name: mysql - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_port: {{ MYSQL_PORT }} - container_port: {{ MYSQL_PORT }} - replicas: 1 - image: mysql:5.6 - env: - - name: MYSQL_ROOT_PASSWORD - value: {{ MYSQL_PASSWORD }} - volumes: - - mount_path: /var/lib/mysql - gcePersistentDisk: - pdName: {{ MYSQL_DISK }} - fsType: {{ MYSQL_DISK_FSTYPE }} -- name: wordpress-php - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_name: wordpress-php - service_port: {{ WORDPRESS_PHP_PORT }} - container_port: {{ WORDPRESS_PHP_PORT }} - replicas: 2 - image: wordpress:fpm - env: - - name: WORDPRESS_DB_PASSWORD - value: {{ MYSQL_PASSWORD }} - - name: WORDPRESS_DB_HOST - value: mysql-service - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs diff --git a/examples/wordpress/wordpress.jinja.schema b/examples/wordpress/wordpress.jinja.schema deleted file mode 100644 index 924ba07ba..000000000 --- a/examples/wordpress/wordpress.jinja.schema +++ /dev/null @@ -1,69 +0,0 @@ -info: - title: Wordpress - description: | - Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service. - - The nginx service and the Wordpress-php service both use NFS to share files. - -properties: - project: - type: string - default: kubernetes-charts - description: Project location to load the images from. - nfs-service: - type: object - properties: - ip: - type: string - default: 10.0.253.247 - description: The IP of the NFS service. - port: - type: int - default: 2049 - description: The port of the NFS service. - disk: - type: string - default: nfs-disk - description: The name of the persistent disk the NFS service uses. - fstype: - type: string - default: ext4 - description: The filesystem the disk of the NFS service uses. - nginx: - type: object - properties: - replicas: - type: int - default: 2 - description: The number of replicas for the nginx service. - wordpress-php: - type: object - properties: - replicas: - type: int - default: 2 - description: The number of replicas for the wordpress-php service. - port: - type: int - default: 9000 - description: The port the wordpress-php service runs on. - mysql: - type: object - properties: - port: - type: int - default: 3306 - description: The port the MySQL service runs on. - password: - type: string - default: mysql-password - description: The root password of the MySQL service. - disk: - type: string - default: mysql-disk - description: The name of the persistent disk the MySQL service uses. - fstype: - type: string - default: ext4 - description: The filesystem the disk of the MySQL service uses. - diff --git a/examples/wordpress/wordpress.yaml b/examples/wordpress/wordpress.yaml deleted file mode 100644 index b401897ab..000000000 --- a/examples/wordpress/wordpress.yaml +++ /dev/null @@ -1,6 +0,0 @@ -imports: -- path: wordpress.jinja - -resources: -- name: wordpress - type: wordpress.jinja diff --git a/expansion/expansion.py b/expansion/expansion.py deleted file mode 100755 index ae8c4fe19..000000000 --- a/expansion/expansion.py +++ /dev/null @@ -1,394 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -"""Template expansion utilities.""" - -import os.path -import sys -import traceback - -import jinja2 -import yaml - -from sandbox_loader import FileAccessRedirector - -import schema_validation - - -def Expand(config, imports=None, env=None, validate_schema=False): - """Expand the configuration with imports. - - Args: - config: string, the raw config to be expanded. - imports: map from import file name, e.g. "helpers/constants.py" to - its contents. - env: map from string to string, the map of environment variable names - to their values - validate_schema: True to run schema validation; False otherwise - Returns: - YAML containing the expanded configuration and its layout, - in the following format: - - config: - ... - layout: - ... - - Raises: - ExpansionError: if there is any error occurred during expansion - """ - try: - return _Expand(config, imports=imports, env=env, - validate_schema=validate_schema) - except Exception as e: - # print traceback.format_exc() - raise ExpansionError('config', str(e)) - - -def _Expand(config, imports=None, env=None, validate_schema=False): - """Expand the configuration with imports.""" - - FileAccessRedirector.redirect(imports) - - yaml_config = None - try: - yaml_config = yaml.safe_load(config) - except yaml.scanner.ScannerError as e: - # Here we know that YAML parser could not parse the template - # we've given it. YAML raises a ScannerError that specifies which file - # had the problem, as well as line and column, but since we're giving - # it the template from string, error message contains <string>, which - # is not very helpful on the user end, so replace it with word - # "template" and make it obvious that YAML contains a syntactic error. - msg = str(e).replace('"<string>"', 'template') - raise Exception('Error parsing YAML: %s' % msg) - - # Handle empty file case - if not yaml_config: - return '' - - # If the configuration does not have ':' in it, the yaml_config will be a - # string. If this is the case just return the str. The code below it - # assumes yaml_config is a map for common cases. - if type(yaml_config) is str: - return yaml_config - - if 'resources' not in yaml_config or yaml_config['resources'] is None: - yaml_config['resources'] = [] - - config = {'resources': []} - layout = {'resources': []} - - _ValidateUniqueNames(yaml_config['resources']) - - # Iterate over all the resources to process. - for resource in yaml_config['resources']: - processed_resource = _ProcessResource(resource, imports, env, - validate_schema) - - config['resources'].extend(processed_resource['config']['resources']) - layout['resources'].append(processed_resource['layout']) - - result = {'config': config, 'layout': layout} - return yaml.safe_dump(result, default_flow_style=False) - - -def _ProcessResource(resource, imports, env, validate_schema=False): - """Processes a resource and expands if template. - - Args: - resource: the resource to be processed, as a map. - imports: map from string to string, the map of imported files names - and contents - env: map from string to string, the map of environment variable names - to their values - validate_schema: True to run schema validation; False otherwise - Returns: - A map containing the layout and configuration of the expanded - resource and any sub-resources, in the format: - - {'config': ..., 'layout': ...} - Raises: - ExpansionError: if there is any error occurred during expansion - """ - # A resource has to have to a name. - if 'name' not in resource: - raise ExpansionError(resource, 'Resource does not have a name.') - - # A resource has to have a type. - if 'type' not in resource: - raise ExpansionError(resource, 'Resource does not have type defined.') - - config = {'resources': []} - # Initialize layout with basic resource information. - layout = {'name': resource['name'], - 'type': resource['type']} - - if resource['type'] in imports: - # A template resource, which contains sub-resources. - expanded_template = ExpandTemplate(resource, imports, - env, validate_schema) - - if expanded_template['resources']: - _ValidateUniqueNames(expanded_template['resources'], - resource['type']) - - # Process all sub-resources of this template. - for resource_to_process in expanded_template['resources']: - processed_resource = _ProcessResource(resource_to_process, - imports, env, - validate_schema) - - # Append all sub-resources to the config resources, - # and the resulting layout of sub-resources. - config['resources'].extend(processed_resource['config'] - ['resources']) - - # Lazy-initialize resources key here because it is not set for - # non-template layouts. - if 'resources' not in layout: - layout['resources'] = [] - layout['resources'].append(processed_resource['layout']) - - if 'properties' in resource: - layout['properties'] = resource['properties'] - else: - # A normal resource has only itself for config. - config['resources'] = [resource] - - return {'config': config, - 'layout': layout} - - -def _ValidateUniqueNames(template_resources, template_name='config'): - """Make sure that every resource name in the given template is unique.""" - names = set() - # Validate that every resource name is unique - for resource in template_resources: - if 'name' in resource: - if resource['name'] in names: - raise ExpansionError( - resource, - 'Resource name \'%s\' is not unique in %s.' - % (resource['name'], template_name)) - names.add(resource['name']) - # If this resource doesn't have a name, we will report that error later - - -def ExpandTemplate(resource, imports, env, validate_schema=False): - """Expands a template, calling expansion mechanism based on type. - - Args: - resource: resource object, the resource that contains parameters to the - jinja file - imports: map from string to string, the map of imported files names - and contents - env: map from string to string, the map of environment variable names - to their values - validate_schema: True to run schema validation; False otherwise - Returns: - The final expanded template - - Raises: - ExpansionError: if there is any error occurred during expansion - """ - source_file = resource['type'] - path = resource['type'] - - # Look for Template in imports. - if source_file not in imports: - raise ExpansionError( - source_file, - 'Unable to find source file %s in imports.' % (source_file)) - - # source_file could be a short version of the template - # say github short name) so we need to potentially map this into - # the fully resolvable name. - if 'path' in imports[source_file] and imports[source_file]['path']: - path = imports[source_file]['path'] - - resource['imports'] = imports - - # Populate the additional environment variables. - if env is None: - env = {} - env['name'] = resource['name'] - env['type'] = resource['type'] - resource['env'] = env - - schema = source_file + '.schema' - if validate_schema and schema in imports: - properties = resource['properties'] if 'properties' in resource else {} - try: - resource['properties'] = schema_validation.Validate( - properties, schema, source_file, imports) - except schema_validation.ValidationErrors as e: - raise ExpansionError(resource['name'], e.message) - - if path.endswith('jinja') or path.endswith('yaml'): - expanded_template = ExpandJinja( - source_file, imports[source_file]['content'], resource, imports) - elif path.endswith('py'): - # This is a Python template. - expanded_template = ExpandPython( - imports[source_file]['content'], source_file, resource) - else: - # The source file is not a jinja file or a python file. - # This in fact should never happen due to the IsTemplate check above. - raise ExpansionError( - resource['source'], - 'Unsupported source file: %s.' % (source_file)) - - parsed_template = yaml.safe_load(expanded_template) - - if parsed_template is None or 'resources' not in parsed_template: - raise ExpansionError(resource['type'], - 'Template did not return a \'resources:\' field.') - - return parsed_template - - -def ExpandJinja(file_name, source_template, resource, imports): - """Render the jinja template using jinja libraries. - - Args: - file_name: - string, the file name. - source_template: - string, the content of jinja file to be render - resource: - resource object, the resource that contains parameters to the - jinja file - imports: - map from string to map {name, path}, the map of imported - files names fully resolved path and contents - Returns: - The final expanded template - Raises: - ExpansionError in case we fail to expand the Jinja2 template. - """ - - try: - jinja_imports = ( - {i['path']: i['content'] for _, i in imports.iteritems()}) - env = jinja2.Environment(loader=jinja2.DictLoader(jinja_imports)) - - template = env.from_string(source_template) - - if ('properties' in resource or 'env' in resource or - 'imports' in resource): - return template.render(resource) - else: - return template.render() - except Exception: - st = 'Exception in %s\n%s' % (file_name, traceback.format_exc()) - raise ExpansionError(file_name, st) - - -def ExpandPython(python_source, file_name, params): - """Run python script to get the expanded template. - - Args: - python_source: string, the python source file to run - file_name: string, the name of the python source file - params: object that contains 'imports' and 'params', the parameters to - the python script - Returns: - The final expanded template. - """ - - try: - # Compile the python code to be run. - constructor = {} - compiled_code = compile(python_source, '<string>', 'exec') - exec compiled_code in constructor # pylint: disable=exec-used - - # Construct the parameters to the python script. - evaluation_context = PythonEvaluationContext(params) - - return constructor['GenerateConfig'](evaluation_context) - except Exception: - st = 'Exception in %s\n%s' % (file_name, traceback.format_exc()) - raise ExpansionError(file_name, st) - - -class PythonEvaluationContext(object): - """The python evaluation context. - - Attributes: - params -- the parameters to be used in the expansion - """ - - def __init__(self, params): - if 'properties' in params: - self.properties = params['properties'] - else: - self.properties = None - - if 'imports' in params: - self.imports = params['imports'] - else: - self.imports = None - - if 'env' in params: - self.env = params['env'] - else: - self.env = None - - -class ExpansionError(Exception): - """Exception raised for errors during expansion process. - - Attributes: - resource: the resource processed that results in the error - message: the detailed message of the error - """ - - def __init__(self, resource, message): - self.resource = resource - self.message = message + ' Resource: ' + str(resource) - super(ExpansionError, self).__init__(self.message) - - -def main(): - if len(sys.argv) < 2: - print >> sys.stderr, 'No input specified.' - sys.exit(1) - template = sys.argv[1] - idx = 2 - imports = {} - while idx < len(sys.argv): - if idx + 1 == len(sys.argv): - print >>sys.stderr, 'Invalid import definition at argv pos %d' \ - % idx - sys.exit(1) - name = sys.argv[idx] - path = sys.argv[idx + 1] - value = sys.argv[idx + 2] - imports[name] = {'content': value, 'path': path} - idx += 3 - - env = {} - # env['deployment'] = os.environ['DEPLOYMENT_NAME'] - # env['project'] = os.environ['PROJECT'] - - validate_schema = 'VALIDATE_SCHEMA' in os.environ - - # Call the expansion logic to actually expand the template. - print Expand(template, imports, env=env, validate_schema=validate_schema) - -if __name__ == '__main__': - main() diff --git a/expansion/expansion_test.py b/expansion/expansion_test.py deleted file mode 100644 index 580a49ab5..000000000 --- a/expansion/expansion_test.py +++ /dev/null @@ -1,511 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -"""Basic unit tests for template expansion library.""" - -import expansion -import os -import unittest -import yaml - - -def GetFilePath(): - """Find our source and data files.""" - return os.path.dirname(os.path.abspath(__file__)) - - -def ReadTestFile(filename): - """Returns contents of a file from the test/ directory.""" - - full_path = GetFilePath() + '/../test/templates/' + filename - test_file = open(full_path, 'r') - return test_file.read() - - -def GetTestBasePath(filename): - """Returns the base path of a file from the testdata/ directory.""" - - full_path = GetFilePath() + '/../test/templates/' + filename - return os.path.dirname(full_path) - - -class ExpansionTest(unittest.TestCase): - """Tests basic functionality of the template expansion library.""" - - EMPTY_RESPONSE = 'config:\n resources: []\nlayout:\n resources: []\n' - - def testEmptyExpansion(self): - template = '' - expanded_template = expansion.Expand( - template) - - self.assertEqual('', expanded_template) - - def testNoResourcesList(self): - template = 'imports: [ test.import ]' - expanded_template = expansion.Expand( - template) - - self.assertEqual(self.EMPTY_RESPONSE, expanded_template) - - def testResourcesListEmpty(self): - template = 'resources:' - expanded_template = expansion.Expand( - template) - - self.assertEqual(self.EMPTY_RESPONSE, expanded_template) - - def testSimpleNoExpansionTemplate(self): - template = ReadTestFile('simple.yaml') - - expanded_template = expansion.Expand( - template) - - result_file = ReadTestFile('simple_result.yaml') - self.assertEquals(result_file, expanded_template) - - def testJinjaExpansion(self): - template = ReadTestFile('jinja_template.yaml') - - imports = {} - imports['jinja_template.jinja'] = ReadTestFile('jinja_template.jinja') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('jinja_template_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testJinjaWithNoParamsExpansion(self): - template = ReadTestFile('jinja_noparams.yaml') - - imports = {} - imports['jinja_noparams.jinja'] = ReadTestFile('jinja_noparams.jinja') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('jinja_noparams_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonWithNoParamsExpansion(self): - template = ReadTestFile('python_noparams.yaml') - - imports = {} - imports['python_noparams.py'] = ReadTestFile('python_noparams.py') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('python_noparams_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonExpansion(self): - template = ReadTestFile('python_template.yaml') - - imports = {} - imports['python_template.py'] = ReadTestFile('python_template.py') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('python_template_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonAndJinjaExpansion(self): - template = ReadTestFile('python_and_jinja_template.yaml') - - imports = {} - imports['python_and_jinja_template.py'] = ReadTestFile( - 'python_and_jinja_template.py') - - imports['python_and_jinja_template.jinja'] = ReadTestFile( - 'python_and_jinja_template.jinja') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('python_and_jinja_template_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testNoImportErrors(self): - template = 'resources: \n- type: something.jinja\n name: something' - expansion.Expand(template, {}) - - def testInvalidConfig(self): - template = ReadTestFile('invalid_config.yaml') - - try: - expansion.Expand( - template) - self.fail('Expansion should fail') - except expansion.ExpansionError as e: - self.assertNotIn(os.path.basename(expansion.__name__), e.message, - 'Do not leak internals') - - def testJinjaWithImport(self): - template = ReadTestFile('jinja_template_with_import.yaml') - - imports = {} - imports['jinja_template_with_import.jinja'] = ReadTestFile( - 'jinja_template_with_import.jinja') - imports['helpers/common.jinja'] = ReadTestFile( - 'helpers/common.jinja') - - yaml_template = yaml.safe_load(template) - - expanded_template = expansion.Expand( - str(yaml_template), imports) - - result_file = ReadTestFile('jinja_template_with_import_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testJinjaWithInlinedFile(self): - template = ReadTestFile('jinja_template_with_inlinedfile.yaml') - - imports = {} - imports['jinja_template_with_inlinedfile.jinja'] = ReadTestFile( - 'jinja_template_with_inlinedfile.jinja') - imports['helpers/common.jinja'] = ReadTestFile( - 'helpers/common.jinja') - - imports['description_text.txt'] = ReadTestFile('description_text.txt') - - yaml_template = yaml.safe_load(template) - - expanded_template = expansion.Expand( - str(yaml_template), imports) - - result_file = \ - ReadTestFile('jinja_template_with_inlinedfile_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonWithImport(self): - template = ReadTestFile('python_template_with_import.yaml') - - imports = {} - imports['python_template_with_import.py'] = ReadTestFile( - 'python_template_with_import.py') - - imports['helpers/common.py'] = ReadTestFile('helpers/common.py') - imports['helpers/extra/common2.py'] = ReadTestFile( - 'helpers/extra/common2.py') - imports['helpers/extra'] = ReadTestFile('helpers/extra/__init__.py') - - yaml_template = yaml.safe_load(template) - - expanded_template = expansion.Expand( - str(yaml_template), imports) - - result_file = ReadTestFile('python_template_with_import_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonWithInlinedFile(self): - template = ReadTestFile('python_template_with_inlinedfile.yaml') - - imports = {} - imports['python_template_with_inlinedfile.py'] = ReadTestFile( - 'python_template_with_inlinedfile.py') - - imports['helpers/common.py'] = ReadTestFile('helpers/common.py') - imports['helpers/extra/common2.py'] = ReadTestFile( - 'helpers/extra/common2.py') - - imports['description_text.txt'] = ReadTestFile('description_text.txt') - - yaml_template = yaml.safe_load(template) - - expanded_template = expansion.Expand( - str(yaml_template), imports) - - result_file = ReadTestFile( - 'python_template_with_inlinedfile_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonWithEnvironment(self): - template = ReadTestFile('python_template_with_env.yaml') - - imports = {} - imports['python_template_with_env.py'] = ReadTestFile( - 'python_template_with_env.py') - - env = {'project': 'my-project'} - - expanded_template = expansion.Expand( - template, imports, env) - - result_file = ReadTestFile('python_template_with_env_result.yaml') - self.assertEquals(result_file, expanded_template) - - def testJinjaWithEnvironment(self): - template = ReadTestFile('jinja_template_with_env.yaml') - - imports = {} - imports['jinja_template_with_env.jinja'] = ReadTestFile( - 'jinja_template_with_env.jinja') - - env = {'project': 'test-project', 'deployment': 'test-deployment'} - - expanded_template = expansion.Expand( - template, imports, env) - - result_file = ReadTestFile('jinja_template_with_env_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testMissingNameErrors(self): - template = 'resources: \n- type: something.jinja\n' - - try: - expansion.Expand(template, {}) - self.fail('Expansion should fail') - except expansion.ExpansionError as e: - self.assertTrue('not have a name' in e.message) - - def testDuplicateNamesErrors(self): - template = ReadTestFile('duplicate_names.yaml') - - try: - expansion.Expand(template, {}) - self.fail('Expansion should fail') - except expansion.ExpansionError as e: - self.assertTrue(("Resource name 'my_instance' is not unique" - " in config.") in e.message) - - def testDuplicateNamesInSubtemplates(self): - template = ReadTestFile('duplicate_names_in_subtemplates.yaml') - - imports = {} - imports['duplicate_names_in_subtemplates.jinja'] = ReadTestFile( - 'duplicate_names_in_subtemplates.jinja') - - try: - expansion.Expand( - template, imports) - self.fail('Expansion should fail') - except expansion.ExpansionError as e: - self.assertTrue('not unique in \ - duplicate_names_in_subtemplates.jinja' - in e.message) - - def testDuplicateNamesMixedLevel(self): - template = ReadTestFile('duplicate_names_mixed_level.yaml') - - imports = {} - imports['duplicate_names_B.jinja'] = ReadTestFile( - 'duplicate_names_B.jinja') - imports['duplicate_names_C.jinja'] = ReadTestFile( - 'duplicate_names_C.jinja') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('duplicate_names_mixed_level_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testDuplicateNamesParentChild(self): - template = ReadTestFile('duplicate_names_parent_child.yaml') - - imports = {} - imports['duplicate_names_B.jinja'] = ReadTestFile( - 'duplicate_names_B.jinja') - - expanded_template = expansion.Expand( - template, imports) - - result_file = ReadTestFile('duplicate_names_parent_child_result.yaml') - - self.assertEquals(result_file, expanded_template) - # Note, this template will fail in the frontend - # for duplicate resource names - - def testTemplateReturnsEmpty(self): - template = ReadTestFile('no_resources.yaml') - - imports = {} - imports['no_resources.py'] = ReadTestFile( - 'no_resources.py') - - try: - expansion.Expand( - template, imports) - self.fail('Expansion should fail') - except expansion.ExpansionError as e: - self.assertIn('Template did not return a \'resources:\' field.', - e.message) - self.assertIn('no_resources.py', e.message) - - def testJinjaDefaultsSchema(self): - # Loop 1000 times to make sure we don't rely on dictionary ordering. - for unused_x in range(0, 1000): - template = ReadTestFile('jinja_defaults.yaml') - - imports = {} - imports['jinja_defaults.jinja'] = ReadTestFile( - 'jinja_defaults.jinja') - imports['jinja_defaults.jinja.schema'] = ReadTestFile( - 'jinja_defaults.jinja.schema') - - expanded_template = expansion.Expand( - template, imports, - validate_schema=True) - - result_file = ReadTestFile('jinja_defaults_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testPythonDefaultsOverrideSchema(self): - template = ReadTestFile('python_schema.yaml') - - imports = {} - imports['python_schema.py'] = ReadTestFile('python_schema.py') - imports['python_schema.py.schema'] = \ - ReadTestFile('python_schema.py.schema') - - env = {'project': 'my-project'} - - expanded_template = expansion.Expand( - template, imports, env=env, - validate_schema=True) - - result_file = ReadTestFile('python_schema_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testJinjaMissingRequiredPropertySchema(self): - template = ReadTestFile('jinja_missing_required.yaml') - - imports = {} - imports['jinja_missing_required.jinja'] = ReadTestFile( - 'jinja_missing_required.jinja') - imports['jinja_missing_required.jinja.schema'] = ReadTestFile( - 'jinja_missing_required.jinja.schema') - - try: - expansion.Expand( - template, imports, - validate_schema=True) - self.fail('Expansion error expected') - except expansion.ExpansionError as e: - self.assertIn('Invalid properties', e.message) - self.assertIn("'important' is a required property", e.message) - self.assertIn('jinja_missing_required_resource_name', e.message) - - def testJinjaErrorFileMessage(self): - template = ReadTestFile('jinja_unresolved.yaml') - - imports = {} - imports['jinja_unresolved.jinja'] = \ - ReadTestFile('jinja_unresolved.jinja') - - try: - expansion.Expand( - template, imports, - validate_schema=False) - self.fail('Expansion error expected') - except expansion.ExpansionError as e: - self.assertIn('jinja_unresolved.jinja', e.message) - - def testJinjaMultipleErrorsSchema(self): - template = ReadTestFile('jinja_multiple_errors.yaml') - - imports = {} - imports['jinja_multiple_errors.jinja'] = ReadTestFile( - 'jinja_multiple_errors.jinja') - imports['jinja_multiple_errors.jinja.schema'] = ReadTestFile( - 'jinja_multiple_errors.jinja.schema') - - try: - expansion.Expand( - template, imports, - validate_schema=True) - self.fail('Expansion error expected') - except expansion.ExpansionError as e: - self.assertIn('Invalid properties', e.message) - self.assertIn("'a string' is not of type 'integer'", e.message) - self.assertIn("'d' is not one of ['a', 'b', 'c']", e.message) - self.assertIn("'longer than 10 chars' is too long", e.message) - self.assertIn("{'multipleOf': 2} is not allowed for 6", e.message) - - def testPythonBadSchema(self): - template = ReadTestFile('python_bad_schema.yaml') - - imports = {} - imports['python_bad_schema.py'] = ReadTestFile( - 'python_bad_schema.py') - imports['python_bad_schema.py.schema'] = ReadTestFile( - 'python_bad_schema.py.schema') - - try: - expansion.Expand( - template, imports, - validate_schema=True) - self.fail('Expansion error expected') - except expansion.ExpansionError as e: - self.assertIn('Invalid schema', e.message) - self.assertIn("'int' is not valid under any of the given schemas", - e.message) - self.assertIn("'maximum' is a dependency of u'exclusiveMaximum'", - e.message) - self.assertIn("10 is not of type u'boolean'", e.message) - self.assertIn("'not a list' is not of type u'array'", e.message) - - def testNoProperties(self): - template = ReadTestFile('no_properties.yaml') - - imports = {} - imports['no_properties.py'] = ReadTestFile( - 'no_properties.py') - - expanded_template = expansion.Expand( - template, imports, - validate_schema=True) - - result_file = ReadTestFile('no_properties_result.yaml') - - self.assertEquals(result_file, expanded_template) - - def testNestedTemplateSchema(self): - template = ReadTestFile('use_helper.yaml') - - imports = {} - imports['use_helper.jinja'] = ReadTestFile( - 'use_helper.jinja') - imports['use_helper.jinja.schema'] = ReadTestFile( - 'use_helper.jinja.schema') - imports['helper.jinja'] = ReadTestFile( - 'helper.jinja') - imports['helper.jinja.schema'] = ReadTestFile( - 'helper.jinja.schema') - - expanded_template = expansion.Expand( - template, imports, - validate_schema=True) - - result_file = ReadTestFile('use_helper_result.yaml') - - self.assertEquals(result_file, expanded_template) - -if __name__ == '__main__': - unittest.main() diff --git a/expansion/file_expander.py b/expansion/file_expander.py deleted file mode 100644 index 2e1276896..000000000 --- a/expansion/file_expander.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -"""App allowing expansion from file names instead of cmdline arguments.""" -import os.path -import sys -from expansion import Expand - - -def main(): - if len(sys.argv) < 2: - print >>sys.stderr, 'No template specified.' - sys.exit(1) - template = '' - imports = {} - try: - with open(sys.argv[1]) as f: - template = f.read() - for imp in sys.argv[2:]: - import_contents = '' - with open(imp) as f: - import_contents = f.read() - import_name = os.path.basename(imp) - imports[import_name] = import_contents - except IOError as e: - print 'IOException: ', str(e) - sys.exit(1) - - env = {} - env['deployment'] = os.environ['DEPLOYMENT_NAME'] - env['project'] = os.environ['PROJECT'] - validate_schema = 'VALIDATE_SCHEMA' in os.environ - - print Expand(template, imports, env=env, validate_schema=validate_schema) - - -if __name__ == '__main__': - main() diff --git a/expansion/requirements.txt b/expansion/requirements.txt deleted file mode 100644 index 0850c2d34..000000000 --- a/expansion/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pyyaml -Jinja2 -Jsonschema diff --git a/expansion/sandbox_loader.py b/expansion/sandbox_loader.py deleted file mode 100644 index d36c82cad..000000000 --- a/expansion/sandbox_loader.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. -###################################################################### - -"""Loader for loading modules from a user provided dictionary of imports.""" - -import imp -from os import sep -import os.path -import sys - - -_IMPORTS = {} - - -class AllowedImportsLoader(object): - - def load_module(self, name, etc=None): # pylint: disable=unused-argument - """Implements loader.load_module() - for loading user provided imports.""" - - module = imp.new_module(name) - content = _IMPORTS[name] - - if content is None: - module.__path__ = [name.replace('.', '/')] - else: - # Run the module code. - exec content in module.__dict__ # pylint: disable=exec-used - - # Register the module so Python code will find it. - sys.modules[name] = module - return module - - -class AllowedImportsHandler(object): - - def find_module(self, name, path=None): # pylint: disable=unused-argument - if name in _IMPORTS: - return AllowedImportsLoader() - else: - return None # Delegate to system handlers. - - -class FileAccessRedirector(object): - - @staticmethod - def redirect(imports): - """Restricts imports and builtin 'open' to the set of user provided imports. - - Imports already available in sys.modules will continue to be available. - - Args: - imports: map from string to dict, the map of files from names. - """ - if imports is not None: - # Build map of fully qualified module names to either the content - # of that module (if it is a file within a package) or just None if - # the module is a package (i.e. a directory). - for name, entry in imports.iteritems(): - path = entry['path'] - content = entry['content'] - prefix, ext = os.path.splitext(os.path.normpath(path)) - if ext not in {'.py', '.pyc'}: - continue - if '.' in prefix: - # Python modules cannot contain '.', ignore these files. - continue - parts = prefix.split(sep) - dirs = ('.'.join(parts[0:i]) for i in xrange(0, len(parts))) - for d in dirs: - if d not in _IMPORTS: - _IMPORTS[d] = None - _IMPORTS['.'.join(parts)] = content - - # Prepend our module handler before standard ones. - sys.meta_path = [AllowedImportsHandler()] + sys.meta_path diff --git a/expansion/schema_validation.py b/expansion/schema_validation.py deleted file mode 100644 index cb9d88996..000000000 --- a/expansion/schema_validation.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -"""Validation of Template properties for deployment manager v2.""" - -import jsonschema -import yaml - -import schema_validation_utils - - -IMPORTS = "imports" -PROPERTIES = "properties" - - -# This validator will set default values in properties. -# This does not return a complete set of errors; use only for setting defaults. -# Pass this object a schema to get a validator for that schema. -DEFAULT_SETTER = schema_validation_utils.ExtendWithDefault( - jsonschema.Draft4Validator) - -# This is a regular validator, use after using the DEFAULT_SETTER -# Pass this object a schema to get a validator for that schema. -VALIDATOR = jsonschema.Draft4Validator - -# This is a validator using the default Draft4 metaschema, -# use it to validate user schemas. -SCHEMA_VALIDATOR = jsonschema.Draft4Validator( - jsonschema.Draft4Validator.META_SCHEMA) - -# JsonSchema to be used to validate the user's "imports:" section -IMPORT_SCHEMA = """ - properties: - imports: - type: array - items: - type: object - required: - - path - properties: - path: - type: string - name: - type: string - additionalProperties: false - uniqueItems: true -""" -# Validator to be used against the "imports:" section of a schema -IMPORT_SCHEMA_VALIDATOR = jsonschema.Draft4Validator( - yaml.safe_load(IMPORT_SCHEMA)) - - -def _ValidateSchema(schema, validating_imports, schema_name, template_name): - """Validate that the passed in schema file is correctly formatted. - - Args: - schema: - contents of the schema file - validating_imports: - boolean, if we should validate the 'imports' section of the schema - schema_name: - name of the schema file to validate - template_name: - name of the template whose properties are being validated - - Raises: - ValidationErrors: A list of ValidationError errors that occured when - validating the schema file - """ - schema_errors = [] - - # Validate the syntax of the optional "imports:" section of the schema - if validating_imports: - schema_errors.extend(IMPORT_SCHEMA_VALIDATOR.iter_errors(schema)) - - # Validate the syntax of the jsonSchema section of the schema - try: - schema_errors.extend(SCHEMA_VALIDATOR.iter_errors(schema)) - except jsonschema.RefResolutionError as e: - # Calls to iter_errors could throw a RefResolution exception - raise ValidationErrors(schema_name, template_name, - [e], is_schema_error=True) - - if schema_errors: - raise ValidationErrors(schema_name, template_name, - schema_errors, is_schema_error=True) - - -def Validate(properties, schema_name, template_name, imports): - """Given a set of properties, validates it against the given schema. - - Args: - properties: - dict, the properties to be validated - schema_name: - name of the schema file to validate - template_name: - name of the template whose properties are being validated - imports: - the map of imported files names to file contents - - Returns: - Dict containing the validated properties, with defaults filled in - - Raises: - ValidationErrors: A list of ValidationError errors that occurred when - validating the properties and schema, - or if the schema file was not found - """ - if schema_name not in imports: - raise ValidationErrors(schema_name, template_name, - ["Could not find schema file '%s'." % - schema_name]) - - raw_schema = imports[schema_name] - - if properties is None: - properties = {} - - schema = yaml.safe_load(raw_schema['content']) - - # If the schema is empty, do nothing. - if not schema: - return properties - - validating_imports = IMPORTS in schema and schema[IMPORTS] - - # If this doesn't raise any exceptions,we can assume we have a valid schema - _ValidateSchema(schema, validating_imports, schema_name, template_name) - - errors = [] - - # Validate that all files specified as "imports:" were included - if validating_imports: - # We have already validated that "imports:" - # is a list of unique "path/name" maps - for import_object in schema[IMPORTS]: - if "name" in import_object: - import_name = import_object["name"] - else: - import_name = import_object["path"] - - if import_name not in imports: - errors.append(("File '%s' requested in schema '%s' " - "but not included with imports." - % (import_name, schema_name))) - - try: - # This code block uses DEFAULT_SETTER and VALIDATOR for two very - # different purposes. - # DEFAULT_SETTER is based on JSONSchema 4,but uses modified validators: - # - The 'required' validator does nothing - # - The 'properties' validator sets default values on user properties - # With these changes, the validator does not report errors correctly. - # - # So, we do error reporting in two steps: - # 1) Use DEFAULT_SETTER to set default values in the user's properties - # 2) Use the unmodified VALIDATOR to report all of the errors - - # Calling iter_errors mutates properties in place, - # adding default values. - # You must call list()! This is a generator, not a function! - list(DEFAULT_SETTER(schema).iter_errors(properties)) - - # Now that we have default values, validate the properties - errors.extend(list(VALIDATOR(schema).iter_errors(properties))) - - if errors: - raise ValidationErrors(schema_name, template_name, errors) - except jsonschema.RefResolutionError as e: - # Calls to iter_errors could throw a RefResolution exception - raise ValidationErrors(schema_name, template_name, - [e], is_schema_error=True) - except TypeError as e: - raise ValidationErrors( - schema_name, template_name, - [e, "Perhaps you forgot to put 'quotes' \ - around your reference."], - is_schema_error=True) - - return properties - - -class ValidationErrors(Exception): - """Exception raised for errors during validation process. - - The errors could have occured either in the schema xor in the properties - - Attributes: - is_schema_error: Boolean, either an invalid schema, - or invalid properties - errors: List of ValidationError type objects - """ - - def BuildMessage(self): - """Builds a human readable message from a list of jsonschema errors. - - Returns: - A string in a human readable message format. - """ - - if self.is_schema_error: - message = "Invalid schema '%s':\n" % self.schema_name - else: - message = "Invalid properties for '%s':\n" % self.template_name - - for error in self.errors: - if isinstance(error, jsonschema.exceptions.ValidationError): - error_message = error.message - location = list(error.path) - if location and len(location): - error_message += " at " + str(location) - # If location is empty the error happened at - # the root of the schema - else: - error_message = str(error) - - message += error_message + "\n" - - return message - - def __init__(self, schema_name, template_name, - errors, is_schema_error=False): - self.schema_name = schema_name - self.template_name = template_name - self.errors = errors - self.is_schema_error = is_schema_error - self.message = self.BuildMessage() - super(ValidationErrors, self).__init__(self.message) diff --git a/expansion/schema_validation_test.py b/expansion/schema_validation_test.py deleted file mode 100644 index 77590d919..000000000 --- a/expansion/schema_validation_test.py +++ /dev/null @@ -1,619 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -import os -import unittest -import schema_validation -import yaml - -INVALID_PROPERTIES = "Invalid properties for 'template.py'" - - -def GetFilePath(): - """Find our source and data files.""" - return os.path.dirname(os.path.abspath(__file__)) - - -def ReadTestFile(filename): - """Returns contents of a file from the testdata/ directory.""" - - full_path = os.path.join(GetFilePath(), '..', 'test', 'schemas', filename) - return open(full_path, 'r').read() - - -def RawValidate(raw_properties, schema_name, raw_schema): - return ImportsRawValidate(raw_properties, schema_name, - {schema_name: raw_schema}) - - -def ImportsRawValidate(raw_properties, schema_name, import_map): - """Takes raw properties, calls validate and returns yaml properties.""" - properties = yaml.safe_load(raw_properties) - return schema_validation.Validate(properties, schema_name, 'template.py', - import_map) - - -class SchemaValidationTest(unittest.TestCase): - """Tests of the schema portion of the template expansion library.""" - - def testDefaults(self): - schema_name = 'defaults.jinja.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - expected_properties = """ - alpha: alpha - one: 1 - """ - self.assertEqual(yaml.safe_load(expected_properties), - RawValidate(empty_properties, schema_name, schema)) - - def testNestedDefaults(self): - schema_name = 'nested_defaults.py.schema' - schema = ReadTestFile(schema_name) - properties = """ - zone: us-central1-a - disks: - - name: backup # diskType and sizeGb set by default - - name: cache # sizeGb set by default - diskType: pd-ssd - - name: data # Nothing set by default - diskType: pd-ssd - sizeGb: 150 - - name: swap # diskType set by default - sizeGb: 200 - """ - expected_properties = """ - zone: us-central1-a - disks: - - sizeGb: 100 - diskType: pd-standard - name: backup - - sizeGb: 100 - diskType: pd-ssd - name: cache - - sizeGb: 150 - diskType: pd-ssd - name: data - - sizeGb: 200 - diskType: pd-standard - name: swap - """ - self.assertEqual(yaml.safe_load(expected_properties), - RawValidate(properties, schema_name, schema)) - - def testNestedRefDefaults(self): - schema_name = 'ref_nested_defaults.py.schema' - schema = ReadTestFile(schema_name) - properties = """ - zone: us-central1-a - disks: - - name: backup # diskType and sizeGb set by default - - name: cache # sizeGb set by default - diskType: pd-ssd - - name: data # Nothing set by default - diskType: pd-ssd - sizeGb: 150 - - name: swap # diskType set by default - sizeGb: 200 - """ - expected_properties = """ - zone: us-central1-a - disks: - - sizeGb: 100 - diskType: pd-standard - name: backup - - sizeGb: 100 - diskType: pd-ssd - name: cache - - sizeGb: 150 - diskType: pd-ssd - name: data - - sizeGb: 200 - diskType: pd-standard - name: swap - """ - self.assertEqual(yaml.safe_load(expected_properties), - RawValidate(properties, schema_name, schema)) - - def testInvalidDefault(self): - schema_name = 'invalid_default.jinja.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn(INVALID_PROPERTIES, e.message) - self.assertIn("'string' is not of type 'integer' at ['number']", - e.message) - - def testRequiredDefault(self): - schema_name = 'required_default.jinja.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - expected_properties = """ - name: my_name - """ - self.assertEqual(yaml.safe_load(expected_properties), - RawValidate(empty_properties, schema_name, schema)) - - def testRequiredDefaultReference(self): - schema_name = 'req_default_ref.py.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn(INVALID_PROPERTIES, e.message) - self.assertIn("'my_name' is not of type 'integer' at ['number']", - e.message) - - def testDefaultReference(self): - schema_name = 'default_ref.jinja.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - expected_properties = 'number: 1' - - self.assertEqual(yaml.safe_load(expected_properties), - RawValidate(empty_properties, schema_name, schema)) - - def testMissingQuoteInReference(self): - schema_name = 'missing_quote.py.schema' - schema = ReadTestFile(schema_name) - properties = 'number: 1' - - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(2, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("type 'NoneType' is not iterable", e.message) - self.assertIn('around your reference', e.message) - - def testRequiredPropertyMissing(self): - schema_name = 'required.jinja.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn(INVALID_PROPERTIES, e.message) - self.assertIn("'name' is a required property", e.errors[0].message) - - def testRequiredPropertyValid(self): - schema_name = 'required.jinja.schema' - schema = ReadTestFile(schema_name) - properties = """ - name: my-name - """ - self.assertEqual(yaml.safe_load(properties), - RawValidate(properties, schema_name, schema)) - - def testMultipleErrors(self): - schema_name = 'defaults.py.schema' - schema = ReadTestFile(schema_name) - properties = """ - one: not a number - alpha: 12345 - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(2, len(e.errors)) - self.assertIn(INVALID_PROPERTIES, e.message) - self.assertIn("'not a number' is not of type 'integer' at ['one']", - e.message) - self.assertIn("12345 is not of type 'string' at ['alpha']", - e.message) - - def testNumbersValid(self): - schema_name = 'numbers.py.schema' - schema = ReadTestFile(schema_name) - properties = """ - minimum0: 0 - exclusiveMin0: 1 - maximum10: 10 - exclusiveMax10: 9 - even: 20 - odd: 21 - """ - self.assertEquals(yaml.safe_load(properties), - RawValidate(properties, schema_name, schema)) - - def testNumbersInvalid(self): - schema_name = 'numbers.py.schema' - schema = ReadTestFile(schema_name) - properties = """ - minimum0: -1 - exclusiveMin0: 0 - maximum10: 11 - exclusiveMax10: 10 - even: 21 - odd: 20 - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(6, len(e.errors)) - self.assertIn(INVALID_PROPERTIES, e.message) - self.assertIn("-1 is less than the minimum of 0 at ['minimum0']", - e.message) - self.assertIn(('0 is less than or equal to the minimum of 0' - " at ['exclusiveMin0']"), e.message) - self.assertIn("11 is greater than the maximum of 10 at \ - ['maximum10']", e.message) - self.assertIn(('10 is greater than or equal to the maximum of 10' - " at ['exclusiveMax10']"), e.message) - self.assertIn("21 is not a multiple of 2 at ['even']", e.message) - self.assertIn("{'multipleOf': 2} is not allowed for 20 at ['odd']", - e.message) - - def testReference(self): - schema_name = 'reference.jinja.schema' - schema = ReadTestFile(schema_name) - properties = """ - odd: 6 - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn('even', e.message) - self.assertIn('is not allowed for 6', e.message) - - def testBadSchema(self): - schema_name = 'bad.jinja.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(2, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("u'minimum' is a dependency of u'exclusiveMinimum'", - e.message) - self.assertIn("0 is not of type u'boolean'", e.message) - - def testInvalidReference(self): - schema_name = 'invalid_reference.py.schema' - schema = ReadTestFile(schema_name) - properties = 'odd: 1' - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn('Unresolvable JSON pointer', e.message) - - def testInvalidReferenceInSchema(self): - schema_name = 'invalid_reference_schema.py.schema' - schema = ReadTestFile(schema_name) - empty_properties = '' - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn('Unresolvable JSON pointer', e.message) - - def testMetadata(self): - schema_name = 'metadata.py.schema' - schema = ReadTestFile(schema_name) - properties = """ - one: 2 - alpha: beta - """ - self.assertEquals(yaml.safe_load(properties), - RawValidate(properties, schema_name, schema)) - - def testInvalidInput(self): - schema_name = 'schema' - schema = """ - info: - title: Invalid Input - properties: invalid - """ - properties = """ - one: 2 - alpha: beta - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("'invalid' is not of type u'object'", e.message) - - def testPattern(self): - schema_name = 'schema' - schema = r""" - properties: - bad-zone: - pattern: \w+-\w+-\w+ - zone: - pattern: \w+-\w+-\w+ - """ - properties = """ - bad-zone: abc - zone: us-central1-a - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn('Invalid properties', e.message) - self.assertIn("'abc' does not match", e.message) - self.assertIn('bad-zone', e.message) - - def testUniqueItems(self): - schema_name = 'schema' - schema = """ - properties: - bad-list: - type: array - uniqueItems: true - list: - type: array - uniqueItems: true - """ - properties = """ - bad-list: - - a - - b - - a - list: - - a - - b - - c - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn('Invalid properties', e.message) - self.assertIn('has non-unique elements', e.message) - self.assertIn('bad-list', e.message) - - def testUniqueItemsOnString(self): - schema_name = 'schema' - schema = """ - properties: - ok-string: - type: string - uniqueItems: true - string: - type: string - uniqueItems: true - """ - properties = """ - ok-string: aaa - string: abc - """ - self.assertEquals(yaml.safe_load(properties), - RawValidate(properties, schema_name, schema)) - - def testRequiredTopLevel(self): - schema_name = 'schema' - schema = """ - info: - title: Invalid Input - required: - - name - """ - properties = """ - one: 2 - alpha: beta - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn(INVALID_PROPERTIES, e.message) - self.assertIn("'name' is a required property", e.message) - - def testEmptySchemaProperties(self): - schema_name = 'schema' - schema = """ - info: - title: Empty Input - properties: - """ - properties = """ - one: 2 - alpha: beta - """ - try: - RawValidate(properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("None is not of type u'object' at [u'properties']", - e.message) - - def testNoInput(self): - schema = """ - info: - title: No other sections - """ - properties = """ - one: 2 - alpha: beta - """ - self.assertEquals(yaml.safe_load(properties), - RawValidate(properties, 'schema', schema)) - - def testEmptySchema(self): - schema = '' - properties = """ - one: 2 - alpha: beta - """ - self.assertEquals(yaml.safe_load(properties), - RawValidate(properties, 'schema', schema)) - - def testImportPathSchema(self): - schema = """ - imports: - - path: a - - path: path/to/b - name: b - """ - properties = """ - one: 2 - alpha: beta - """ - - import_map = {'schema': schema, - 'a': '', - 'b': ''} - - self.assertEquals(yaml.safe_load(properties), - ImportsRawValidate(properties, 'schema', import_map)) - - def testImportSchemaMissing(self): - schema = '' - empty_properties = '' - - try: - properties = yaml.safe_load(empty_properties) - schema_validation.Validate(properties, 'schema', 'template', - {'wrong_name': schema}) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Could not find schema file 'schema'", e.message) - - def testImportsMalformedNotAList(self): - schema_name = 'schema' - schema = """ - imports: not-a-list - """ - empty_properties = '' - - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("is not of type 'array' at ['imports']", e.message) - - def testImportsMalformedMissingPath(self): - schema_name = 'schema' - schema = """ - imports: - - name: no_path.yaml - """ - empty_properties = '' - - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("'path' is a required property", e.message) - - def testImportsMalformedNonunique(self): - schema_name = 'schema' - schema = """ - imports: - - path: a.yaml - name: a - - path: a.yaml - name: a - """ - empty_properties = '' - - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn('non-unique elements', e.message) - - def testImportsMalformedAdditionalProperties(self): - schema_name = 'schema' - schema = """ - imports: - - path: a.yaml - gnome: a - """ - empty_properties = '' - - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(1, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn('Additional properties are not allowed' - " ('gnome' was unexpected)", e.message) - - def testImportAndInputErrors(self): - schema = """ - imports: - - path: file - required: - - name - """ - empty_properties = '' - - try: - RawValidate(empty_properties, 'schema', schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(2, len(e.errors)) - self.assertIn("'file' requested in schema 'schema'", e.message) - self.assertIn("'name' is a required property", e.message) - - def testImportAndInputSchemaErrors(self): - schema_name = 'schema' - schema = """ - imports: not-a-list - required: not-a-list - """ - empty_properties = '' - - try: - RawValidate(empty_properties, schema_name, schema) - self.fail('Validation should fail') - except schema_validation.ValidationErrors as e: - self.assertEqual(2, len(e.errors)) - self.assertIn("Invalid schema '%s'" % schema_name, e.message) - self.assertIn("is not of type 'array' at ['imports']", e.message) - self.assertIn("is not of type u'array' at [u'required']", - e.message) - -if __name__ == '__main__': - unittest.main() diff --git a/expansion/schema_validation_utils.py b/expansion/schema_validation_utils.py deleted file mode 100644 index 0c5a9a192..000000000 --- a/expansion/schema_validation_utils.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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 for Schema Validation.""" - -import jsonschema - -DEFAULT = 'default' -PROPERTIES = 'properties' -REF = '$ref' -REQUIRED = 'required' - - -def ExtendWithDefault(validator_class): - """Takes a validator and makes it set default values on properties. - - Args: - validator_class: A class to add our overridden validators to - - Returns: - A validator_class that will set default values - and ignore required fields - """ - validate_properties = validator_class.VALIDATORS['properties'] - - def SetDefaultsInProperties(validator, user_schema, user_properties, - parent_schema): - SetDefaults(validator, user_schema or {}, user_properties, - parent_schema, validate_properties) - - return jsonschema.validators.extend( - validator_class, {PROPERTIES: SetDefaultsInProperties, - REQUIRED: IgnoreKeyword}) - - -def SetDefaults(validator, user_schema, user_properties, parent_schema, - validate_properties): - """Populate the default values of properties. - - Args: - validator: A generator that validates the "properties" keyword - of the schema - user_schema: Schema which might define defaults, might be a nested - part of the entire schema file. - user_properties: User provided values which we are setting defaults on - parent_schema: Schema object that contains the schema being - evaluated on this pass, user_schema. - validate_properties: Validator function, called recursively. - """ - - for schema_property, subschema in user_schema.iteritems(): - # The ordering of these conditions assumes that '$ref' blocks override - # all other schema info, which is what the jsonschema library assumes. - - # If the subschema has a reference, - # see if that reference defines a 'default' value - if REF in subschema: - out = ResolveReferencedDefault(validator, subschema[REF]) - user_properties.setdefault(schema_property, out) - # Otherwise, see if the subschema has a 'default' value - elif DEFAULT in subschema: - user_properties.setdefault(schema_property, subschema[DEFAULT]) - - # Recursively apply defaults. This is a generator, we must wrap with list() - list(validate_properties(validator, user_schema, - user_properties, parent_schema)) - - -def ResolveReferencedDefault(validator, ref): - """Resolves a reference, and returns any default value it defines. - - Args: - validator: A generator that validates the "$ref" keyword - ref: The target of the "$ref" keyword - - Returns: - The value of the 'default' field found in the referenced schema, - or None - """ - with validator.resolver.resolving(ref) as resolved: - if DEFAULT in resolved: - return resolved[DEFAULT] - - -def IgnoreKeyword( - unused_validator, unused_required, unused_instance, unused_schema): - """Validator for JsonSchema that does nothing.""" - pass diff --git a/get-install.sh b/get-install.sh deleted file mode 100755 index f975840ae..000000000 --- a/get-install.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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 this from the root of your clone of the kubernetes/helm repository. -# Be sure to checkout the release you want to install before running it, -# since it will attempt to pull the version from HEAD on the current branch. - -set -euo pipefail - -DEFAULT_TAG=git-$(git rev-parse --short HEAD) -DEFAULT_PLATFORM=$(uname | tr '[:upper:]' '[:lower:]') -DEFAULT_ARCH=$(uname -m) - -if [[ "${DEFAULT_ARCH}" == x86_64 ]]; then - DEFAULT_ARCH=amd64 -fi - -PLATFORM=${PLATFORM:-${DEFAULT_PLATFORM}} -ARCH=${ARCH:-${DEFAULT_ARCH}} -TAG=${TAG:-${DEFAULT_TAG}} - -BINARY=helm-${PLATFORM}-${ARCH} -ZIP=${TAG}-${BINARY}.zip - -STORAGE_URL=http://get-helm.storage.googleapis.com - -echo "Downloading ${ZIP}..." -curl -Ls "${STORAGE_URL}/${ZIP}" -O - -unzip -qo ${ZIP} -rm ${ZIP} - -chmod +x helm - -cat <<EOF - -helm is now available in your current directory. - -Before using it, please install the Deployment Manager service in your -kubernetes cluster by running - -$ ./helm server install - -To get started, run: - -$ ./helm help - -EOF diff --git a/glide.lock b/glide.lock deleted file mode 100644 index c70ee8dfc..000000000 --- a/glide.lock +++ /dev/null @@ -1,70 +0,0 @@ -hash: f9b47a1852e40963671d923d7170aa4d730f7d74de94a3c66420a3c1635bc5c5 -updated: 2016-04-01T21:35:30.235550603-04:00 -imports: -- name: github.com/aokoli/goutils - version: 45307ec16e3cd47cd841506c081f7afd8237d210 -- name: github.com/cloudfoundry-incubator/candiedyaml - version: 479485e9bfc69ee37d074b36ce36da5e4fba7941 -- name: github.com/codegangsta/cli - version: bc465becccd1d527002fda095fc3c19d9c115029 -- name: github.com/emicklei/go-restful - version: 402f11d42bfe18198ffd5c68258c631c8fbf2c3c - subpackages: - - log -- name: github.com/ghodss/yaml - version: 1a6f069841556a7bcaff4a397ca6e8328d266c2f -- name: github.com/google/go-github - version: 4403af9a2a0f2c2577be18a928d98f77d5748168 - subpackages: - - github -- name: github.com/gorilla/context - version: 1ea25387ff6f684839d82767c1733ff4d4d15d0a -- name: github.com/gorilla/handlers - version: ee54c7b44cab12289237fb8631314790076e728b -- name: github.com/gorilla/mux - version: 0eeaf8392f5b04950925b8a69fe70f110fa7cbfc -- name: github.com/juju/gojsonpointer - version: afe8b77aa08f272b49e01b82de78510c11f61500 -- name: github.com/juju/gojsonreference - version: f0d24ac5ee330baa21721cdff56d45e4ee42628e -- name: github.com/juju/gojsonschema - version: e1ad140384f254c82f89450d9a7c8dd38a632838 -- name: github.com/Masterminds/httputil - version: e9b977e9cf16f9d339573e18f0f1f7ce5d3f419a -- name: github.com/Masterminds/semver - version: 808ed7761c233af2de3f9729a041d68c62527f3a -- name: github.com/Masterminds/sprig - version: 679bb747f11c6ffc3373965988fea8877c40b47b -- name: golang.org/x/net - version: 3e8a7b0329d536af18e227bb21b6da4d1dbbe180 - subpackages: - - context - - context/ctxhttp -- name: golang.org/x/oauth2 - version: 33fa30fe45020622640e947917fd1fc4c81e3dce - subpackages: - - google - - internal - - jws - - jwt -- name: google.golang.org/api - version: 43c645d4bcf9251ced36c823a93b6d198764aae4 - subpackages: - - storage/v1 - - gensupport - - googleapi - - googleapi/internal/uritemplates -- name: google.golang.org/cloud - version: 8a7fce32d2cdf2d4e19068ecc53164b973b3e958 - subpackages: - - compute/metadata - - internal -- name: gopkg.in/mgo.v2 - version: b6e2fa371e64216a45e61072a96d4e3859f169da - subpackages: - - bson - - internal/sasl - - internal/scram -- name: gopkg.in/yaml.v2 - version: a83829b6f1293c91addabc89d0571c246397bbf4 -devImports: [] diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index eae7a16e8..000000000 --- a/glide.yaml +++ /dev/null @@ -1,24 +0,0 @@ -package: github.com/kubernetes/helm -license: Apache-2.0 -ignore: -- google.golang.org/appengine -import: -- package: github.com/Masterminds/semver - version: ^1.1.0 -- package: github.com/aokoli/goutils - version: ^1.0.0 -- package: github.com/cloudfoundry-incubator/candiedyaml -- package: github.com/codegangsta/cli -- package: github.com/emicklei/go-restful -- package: github.com/ghodss/yaml -- package: github.com/google/go-github - subpackages: - - github -- package: github.com/gorilla/handlers - version: ^1.1 -- package: github.com/gorilla/mux -- package: gopkg.in/yaml.v2 -- package: github.com/Masterminds/sprig - version: ^2.1.0 -- package: github.com/Masterminds/httputil -- package: github.com/juju/gojsonschema diff --git a/hack/README.md b/hack/README.md deleted file mode 100644 index 2f26db573..000000000 --- a/hack/README.md +++ /dev/null @@ -1,24 +0,0 @@ -Collection of convenience scripts -================================= - -[Vagrantfile](Vagrantfile) --------------------------- - -A Vagrantfile to create a standalone build environment for helm. -It is handy if you do not have Golang and the dependencies used by Helm on your local machine. - - $ git clone https://github.com/kubernetes/helm.git - $ cd helm/hack - $ vagrant up - -Once the machine is up, you can SSH to it and start a new build of helm - - $ vagrant ssh - $ cd src/github.com/kubernetes/helm - $ make build - -[dm-push.sh](dm-push.sh) ------------------------- - -Run this from helm root to build and push the dm client plus -kubernetes install config into the publicly readable GCS bucket gs://get-dm. diff --git a/hack/Vagrantfile b/hack/Vagrantfile deleted file mode 100644 index 2f5b4a244..000000000 --- a/hack/Vagrantfile +++ /dev/null @@ -1,49 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -GLIDE_VERSION = "0.9.1" -GO_VERSION = "1.6" - -$bootstrap=<<SCRIPT -apt-get update -apt-get -y install wget -wget -qO- https://get.docker.com/ | sh -gpasswd -a vagrant docker -service docker restart -SCRIPT - -$helm=<<SCRIPT -curl -L https://storage.googleapis.com/golang/go#{GO_VERSION}.linux-amd64.tar.gz -o go#{GO_VERSION}.linux-amd64.tar.gz -tar -C /usr/local -xzf go#{GO_VERSION}.linux-amd64.tar.gz -wget "https://github.com/Masterminds/glide/releases/download/#{GLIDE_VERSION}/glide-#{GLIDE_VERSION}-linux-amd64.tar.gz" -su vagrant -c "mkdir -p /home/vagrant/bin" -su vagrant -c "mkdir -p /home/vagrant/src/github.com/kubernetes/helm" -chgrp vagrant -R /home/vagrant/src -chown vagrant -R /home/vagrant/src -su vagrant -c "tar -vxz -C /home/vagrant/bin --strip=1 -f glide-#{GLIDE_VERSION}-linux-amd64.tar.gz" -su vagrant -c "echo 'export GLIDE_HOME=/home/vagrant/.glide' >> ~/.profile" -su vagrant -c "echo 'export PATH=$PATH:/usr/local/go/bin:/home/vagrant/bin' >> ~/.profile" -su vagrant -c "echo 'export GOPATH=/home/vagrant' >> ~/.profile" -SCRIPT - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # Every Vagrant virtual environment requires a box to build off of. - - config.vm.box = "ubuntu/trusty64" - - config.vm.network "private_network", ip: "192.168.33.10" - - config.vm.provider "virtualbox" do |vb| - vb.customize ["modifyvm", :id, "--memory", "2048"] - end - - config.vm.synced_folder "../", "/home/vagrant/src/github.com/kubernetes/helm" - config.vm.synced_folder ".", "/vagrant", disabled: true - - config.vm.provision :shell, inline: $bootstrap - config.vm.provision :shell, inline: $helm - -end diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go deleted file mode 100644 index 8367bd83a..000000000 --- a/pkg/chart/chart.go +++ /dev/null @@ -1,460 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/kubernetes/helm/pkg/log" -) - -// ChartfileName is the default Chart file name. -const ChartfileName string = "Chart.yaml" - -const ( - preTemplates string = "templates/" - preHooks string = "hooks/" - preDocs string = "docs/" - preIcon string = "icon.svg" -) - -var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") - -// Chart represents a complete chart. -// -// A chart consists of the following parts: -// -// - Chart.yaml: In code, we refer to this as the Chartfile -// - templates/*: The template directory -// - README.md: Optional README file -// - LICENSE: Optional license file -// - hooks/: Optional hooks registry -// - docs/: Optional docs directory -// -// Packed charts are stored in gzipped tar archives (.tgz). Unpackaged charts -// are directories where the directory name is the Chartfile.Name. -// -// Optionally, a chart might also locate a provenance (.prov) file that it -// can use for cryptographic signing. -type Chart struct { - loader chartLoader -} - -// Close the chart. -// -// Charts should always be closed when no longer needed. -func (c *Chart) Close() error { - return c.loader.close() -} - -// Chartfile gets the Chartfile (Chart.yaml) for this chart. -func (c *Chart) Chartfile() *Chartfile { - return c.loader.chartfile() -} - -// Dir returns the directory where the charts are located. -func (c *Chart) Dir() string { - return c.loader.dir() -} - -// DocsDir returns the directory where the chart's documentation is stored. -func (c *Chart) DocsDir() string { - return filepath.Join(c.loader.dir(), preDocs) -} - -// HooksDir returns the directory where the hooks are stored. -func (c *Chart) HooksDir() string { - return filepath.Join(c.loader.dir(), preHooks) -} - -// TemplatesDir returns the directory where the templates are stored. -func (c *Chart) TemplatesDir() string { - return filepath.Join(c.loader.dir(), preTemplates) -} - -// Icon returns the path to the icon.svg file. -// -// If an icon is not found in the chart, this will return an error. -func (c *Chart) Icon() (string, error) { - i := filepath.Join(c.Dir(), preIcon) - _, err := os.Stat(i) - return i, err -} - -// chartLoader provides load, close, and save implementations for a chart. -type chartLoader interface { - // Chartfile resturns a *Chartfile for this chart. - chartfile() *Chartfile - // Dir returns a directory where the chart can be accessed. - dir() string - - // Close cleans up a chart. - close() error -} - -type dirChart struct { - chartyaml *Chartfile - chartdir string -} - -func (d *dirChart) chartfile() *Chartfile { - return d.chartyaml -} - -func (d *dirChart) dir() string { - return d.chartdir -} - -func (d *dirChart) close() error { - return nil -} - -type tarChart struct { - chartyaml *Chartfile - tmpDir string -} - -func (t *tarChart) chartfile() *Chartfile { - return t.chartyaml -} - -func (t *tarChart) dir() string { - return t.tmpDir -} - -func (t *tarChart) close() error { - // Remove the temp directory. - return os.RemoveAll(t.tmpDir) -} - -// Create creates a new chart in a directory. -// -// Inside of dir, this will create a directory based on the name of -// chartfile.Name. It will then write the Chart.yaml into this directory and -// create the (empty) appropriate directories. -// -// The returned *Chart will point to the newly created directory. -// -// If dir does not exist, this will return an error. -// If Chart.yaml or any directories cannot be created, this will return an -// error. In such a case, this will attempt to clean up by removing the -// new chart directory. -func Create(chartfile *Chartfile, dir string) (*Chart, error) { - path, err := filepath.Abs(dir) - if err != nil { - return nil, err - } - - if fi, err := os.Stat(path); err != nil { - return nil, err - } else if !fi.IsDir() { - return nil, fmt.Errorf("no such directory %s", path) - } - - n := fname(chartfile.Name) - cdir := filepath.Join(path, n) - if _, err := os.Stat(cdir); err == nil { - return nil, fmt.Errorf("directory already exists: %s", cdir) - } - if err := os.MkdirAll(cdir, 0755); err != nil { - return nil, err - } - - rollback := func() { - // TODO: Should we log failures here? - os.RemoveAll(cdir) - } - - if err := chartfile.Save(filepath.Join(cdir, ChartfileName)); err != nil { - rollback() - return nil, err - } - - for _, d := range []string{preHooks, preDocs, preTemplates} { - if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil { - rollback() - return nil, err - } - } - - return &Chart{ - loader: &dirChart{chartyaml: chartfile, chartdir: cdir}, - }, nil -} - -// fname prepares names for the filesystem -func fname(name string) string { - // Right now, we don't do anything. Do we need to encode any particular - // characters? What characters are legal in a chart name, but not in file - // names on Windows, Linux, or OSX. - return name -} - -// LoadDir loads an entire chart from a directory. -// -// This includes the Chart.yaml (*Chartfile) and all of the manifests. -// -// If you are just reading the Chart.yaml file, it is substantially more -// performant to use LoadChartfile. -func LoadDir(chart string) (*Chart, error) { - dir, err := filepath.Abs(chart) - if err != nil { - return nil, fmt.Errorf("%s is not a valid path", chart) - } - - if fi, err := os.Stat(dir); err != nil { - return nil, err - } else if !fi.IsDir() { - return nil, fmt.Errorf("%s is not a directory", chart) - } - - cf, err := LoadChartfile(filepath.Join(dir, "Chart.yaml")) - if err != nil { - return nil, err - } - - cl := &dirChart{ - chartyaml: cf, - chartdir: dir, - } - - return &Chart{ - loader: cl, - }, nil -} - -// LoadData loads a chart from data, where data is a []byte containing a gzipped tar file. -func LoadData(data []byte) (*Chart, error) { - return LoadDataFromReader(bytes.NewBuffer(data)) -} - -// Load loads a chart from a chart archive. -// -// A chart archive is a gzipped tar archive that follows the Chart format -// specification. -func Load(archive string) (*Chart, error) { - if fi, err := os.Stat(archive); err != nil { - return nil, err - } else if fi.IsDir() { - return nil, errors.New("cannot load a directory with chart.Load()") - } - - raw, err := os.Open(archive) - if err != nil { - return nil, err - } - defer raw.Close() - - return LoadDataFromReader(raw) -} - -// LoadDataFromReader loads a chart from a reader -func LoadDataFromReader(r io.Reader) (*Chart, error) { - unzipped, err := gzip.NewReader(r) - if err != nil { - return nil, err - } - defer unzipped.Close() - - untarred := tar.NewReader(unzipped) - c, err := loadTar(untarred) - if err != nil { - return nil, err - } - - cf, err := LoadChartfile(filepath.Join(c.tmpDir, ChartfileName)) - if err != nil { - return nil, err - } - c.chartyaml = cf - return &Chart{loader: c}, nil -} - -func loadTar(r *tar.Reader) (*tarChart, error) { - td, err := ioutil.TempDir("", "chart-") - if err != nil { - return nil, err - } - - // ioutil.TempDir uses Getenv("TMPDIR"), so there are no guarantees - dir, err := filepath.Abs(td) - if err != nil { - return nil, fmt.Errorf("%s is not a valid path", td) - } - - c := &tarChart{ - chartyaml: &Chartfile{}, - tmpDir: dir, - } - - firstDir := "" - - hdr, err := r.Next() - for err == nil { - log.Debug("Reading %s", hdr.Name) - - // This is to prevent malformed tar attacks. - hdr.Name = filepath.Clean(hdr.Name) - - if firstDir == "" { - fi := hdr.FileInfo() - if fi.IsDir() { - log.Debug("Discovered app named %s", hdr.Name) - firstDir = hdr.Name - } else { - log.Warn("Unexpected file at root of archive: %s", hdr.Name) - } - } else if strings.HasPrefix(hdr.Name, firstDir) { - log.Debug("Extracting %s to %s", hdr.Name, c.tmpDir) - - // We know this has the prefix, so we know there won't be an error. - rel, _ := filepath.Rel(firstDir, hdr.Name) - - // If tar record is a directory, create one in the tmpdir and return. - if hdr.FileInfo().IsDir() { - os.MkdirAll(filepath.Join(c.tmpDir, rel), 0755) - hdr, err = r.Next() - continue - } - - dest := filepath.Join(c.tmpDir, rel) - f, err := os.Create(filepath.Join(c.tmpDir, rel)) - if err != nil { - log.Warn("Could not create %s: %s", dest, err) - hdr, err = r.Next() - continue - } - if _, err := io.Copy(f, r); err != nil { - log.Warn("Failed to copy %s: %s", dest, err) - } - f.Close() - } else { - log.Warn("Unexpected file outside of chart: %s", hdr.Name) - } - hdr, err = r.Next() - } - - if err != nil && err != io.EOF { - log.Warn("Unexpected error reading tar: %s", err) - c.close() - return c, err - } - log.Info("Reached end of Tar file") - - return c, nil -} - -// Member is a file in a chart. -type Member struct { - Path string `json:"path"` // Path from the root of the chart. - Content []byte `json:"content"` // Base64 encoded content. -} - -// LoadTemplates loads the members of TemplatesDir(). -func (c *Chart) LoadTemplates() ([]*Member, error) { - dir := c.TemplatesDir() - return c.loadDirectory(dir) -} - -// loadDirectory loads the members of a directory. -func (c *Chart) loadDirectory(dir string) ([]*Member, error) { - files, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - - members := []*Member{} - for _, file := range files { - filename := filepath.Join(dir, file.Name()) - if !file.IsDir() { - addition, err := c.loadMember(filename) - if err != nil { - return nil, err - } - - members = append(members, addition) - } else { - additions, err := c.loadDirectory(filename) - if err != nil { - return nil, err - } - - members = append(members, additions...) - } - } - - return members, nil -} - -// LoadMember loads a chart member from a given path where path is the root of the chart. -func (c *Chart) LoadMember(path string) (*Member, error) { - filename := filepath.Join(c.loader.dir(), path) - return c.loadMember(filename) -} - -// loadMember loads and base 64 encodes a file. -func (c *Chart) loadMember(filename string) (*Member, error) { - dir := c.Dir() - if !strings.HasPrefix(filename, dir) { - err := fmt.Errorf("File %s is outside chart directory %s", filename, dir) - return nil, err - } - - content, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - path := strings.TrimPrefix(filename, dir) - path = strings.TrimLeft(path, "/") - result := &Member{ - Path: path, - Content: content, - } - - return result, nil -} - -// Content is abstraction for the contents of a chart. -type Content struct { - Chartfile *Chartfile `json:"chartfile"` - Members []*Member `json:"members"` -} - -// LoadContent loads contents of a chart directory into Content -func (c *Chart) LoadContent() (*Content, error) { - ms, err := c.loadDirectory(c.Dir()) - if err != nil { - return nil, err - } - - cc := &Content{ - Chartfile: c.Chartfile(), - Members: ms, - } - - return cc, nil -} diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go deleted file mode 100644 index e0da71ac6..000000000 --- a/pkg/chart/chart_test.go +++ /dev/null @@ -1,287 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "reflect" - "testing" - - "github.com/kubernetes/helm/pkg/log" - "github.com/kubernetes/helm/pkg/util" -) - -const ( - testfile = "testdata/frobnitz/Chart.yaml" - testdir = "testdata/frobnitz/" - testarchive = "testdata/frobnitz-0.0.1.tgz" - testill = "testdata/ill-1.2.3.tgz" - testnochart = "testdata/nochart.tgz" - testmember = "templates/wordpress.jinja" -) - -// Type canaries. If these fail, they will fail at compile time. -var _ chartLoader = &dirChart{} -var _ chartLoader = &tarChart{} - -func init() { - log.IsDebugging = true -} - -func TestLoadDir(t *testing.T) { - - c, err := LoadDir(testdir) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - } - - if c.Chartfile().Name != "frobnitz" { - t.Errorf("Expected chart name to be 'frobnitz'. Got '%s'.", c.Chartfile().Name) - } - - if c.Chartfile().Dependencies[0].Version != "^3" { - d := c.Chartfile().Dependencies[0].Version - t.Errorf("Expected dependency 0 to have version '^3'. Got '%s'.", d) - } -} - -func TestLoad(t *testing.T) { - c, err := Load(testarchive) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - return - } - defer c.Close() - - if c.Chartfile() == nil { - t.Error("No chartfile was loaded.") - return - } - - if c.Chartfile().Name != "frobnitz" { - t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name) - } -} - -func TestLoadData(t *testing.T) { - data, err := ioutil.ReadFile(testarchive) - if err != nil { - t.Errorf("Failed to read testarchive file: %s", err) - return - } - c, err := LoadData(data) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - return - } - if c.Chartfile() == nil { - t.Error("No chartfile was loaded.") - return - } - - if c.Chartfile().Name != "frobnitz" { - t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name) - } -} - -func TestLoadIll(t *testing.T) { - c, err := Load(testill) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - return - } - defer c.Close() - - if c.Chartfile() == nil { - t.Error("No chartfile was loaded.") - return - } - - // Ill does not have an icon. - if i, err := c.Icon(); err == nil { - t.Errorf("Expected icon to be missing. Got %s", i) - } -} - -func TestLoadNochart(t *testing.T) { - _, err := Load(testnochart) - if err == nil { - t.Error("Nochart should not have loaded at all.") - } -} - -func TestChart(t *testing.T) { - c, err := LoadDir(testdir) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - } - defer c.Close() - - if c.Dir() != c.loader.dir() { - t.Errorf("Unexpected location for directory: %s", c.Dir()) - } - - if c.Chartfile().Name != c.loader.chartfile().Name { - t.Errorf("Unexpected chart file name: %s", c.Chartfile().Name) - } - - dir := c.Dir() - d := c.DocsDir() - if d != filepath.Join(dir, preDocs) { - t.Errorf("Unexpectedly, docs are in %s", d) - } - - d = c.TemplatesDir() - if d != filepath.Join(dir, preTemplates) { - t.Errorf("Unexpectedly, templates are in %s", d) - } - - d = c.HooksDir() - if d != filepath.Join(dir, preHooks) { - t.Errorf("Unexpectedly, hooks are in %s", d) - } - - i, err := c.Icon() - if err != nil { - t.Errorf("No icon found in test chart: %s", err) - } - if i != filepath.Join(dir, preIcon) { - t.Errorf("Unexpectedly, icon is in %s", i) - } -} - -func TestLoadTemplates(t *testing.T) { - c, err := LoadDir(testdir) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - } - - members, err := c.LoadTemplates() - if members == nil { - t.Fatalf("Cannot load templates: unknown error") - } - - if err != nil { - t.Fatalf("Cannot load templates: %s", err) - } - - dir := c.TemplatesDir() - files, err := ioutil.ReadDir(dir) - if err != nil { - t.Fatalf("Cannot read template directory: %s", err) - } - - if len(members) != len(files) { - t.Fatalf("Expected %d templates, got %d", len(files), len(members)) - } - - root := c.loader.dir() - for _, file := range files { - path := filepath.Join(preTemplates, file.Name()) - if err := findMember(root, path, members); err != nil { - t.Fatal(err) - } - } -} - -func findMember(root, path string, members []*Member) error { - for _, member := range members { - if member.Path == path { - filename := filepath.Join(root, path) - if err := compareContent(filename, member.Content); err != nil { - return err - } - - return nil - } - } - - return fmt.Errorf("Template not found: %s", path) -} - -func TestLoadMember(t *testing.T) { - c, err := LoadDir(testdir) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - } - - member, err := c.LoadMember(testmember) - if member == nil { - t.Fatalf("Cannot load member %s: unknown error", testmember) - } - - if err != nil { - t.Fatalf("Cannot load member %s: %s", testmember, err) - } - - if member.Path != testmember { - t.Errorf("Expected member path %s, got %s", testmember, member.Path) - } - - filename := filepath.Join(c.loader.dir(), testmember) - if err := compareContent(filename, member.Content); err != nil { - t.Fatal(err) - } -} - -func TestLoadContent(t *testing.T) { - c, err := LoadDir(testdir) - if err != nil { - t.Errorf("Failed to load chart: %s", err) - } - - content, err := c.LoadContent() - if err != nil { - t.Errorf("Failed to load chart content: %s", err) - } - - want := c.Chartfile() - have := content.Chartfile - if !reflect.DeepEqual(want, have) { - t.Errorf("Unexpected chart file\nwant:\n%s\nhave:\n%s\n", - util.ToYAMLOrError(want), util.ToYAMLOrError(have)) - } - - for _, member := range content.Members { - have := member.Content - wantMember, err := c.LoadMember(member.Path) - if err != nil { - t.Errorf("Failed to load chart member: %s", err) - } - - t.Logf("%s:\n%s\n\n", member.Path, member.Content) - want := wantMember.Content - if !reflect.DeepEqual(want, have) { - t.Errorf("Unexpected chart member %s\nwant:\n%v\nhave:\n%v\n", member.Path, want, have) - } - } -} - -func compareContent(filename string, content []byte) error { - compare, err := ioutil.ReadFile(filename) - if err != nil { - return fmt.Errorf("Cannot read test file %s: %s", filename, err) - } - - if !reflect.DeepEqual(compare, content) { - return fmt.Errorf("Expected member content\n%v\ngot\n%v", compare, content) - } - - return nil -} diff --git a/pkg/chart/chartfile.go b/pkg/chart/chartfile.go deleted file mode 100644 index 93a54940d..000000000 --- a/pkg/chart/chartfile.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "io/ioutil" - - "github.com/Masterminds/semver" - "gopkg.in/yaml.v2" -) - -// Chartfile describes a Helm Chart (e.g. Chart.yaml) -type Chartfile struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - Version string `yaml:"version"` - Keywords []string `yaml:"keywords,omitempty"` - Maintainers []*Maintainer `yaml:"maintainers,omitempty"` - Source []string `yaml:"source,omitempty"` - Home string `yaml:"home"` - Dependencies []*Dependency `yaml:"dependencies,omitempty"` - Environment []*EnvConstraint `yaml:"environment,omitempty"` - Expander *Expander `yaml:"expander,omitempty"` - Schema string `yaml:"schema,omitempty"` -} - -// Maintainer describes a chart maintainer. -type Maintainer struct { - Name string `yaml:"name"` - Email string `yaml:"email,omitempty"` -} - -// Dependency describes a specific dependency. -type Dependency struct { - Name string `yaml:"name,omitempty"` - Version string `yaml:"version"` - Location string `yaml:"location"` -} - -// EnvConstraint specifies environmental constraints. -type EnvConstraint struct { - Name string `yaml:"name"` - Version string `yaml:"version"` - Extensions []string `yaml:"extensions,omitempty"` - APIGroups []string `yaml:"apiGroups,omitempty"` -} - -// Expander controls how template/ is evaluated. -type Expander struct { - // Kubernetes service name to look up in DNS. - Name string `json:"name"` - // During evaluation, which file to start from. - Entrypoint string `json:"entrypoint"` -} - -// LoadChartfile loads a Chart.yaml file into a *Chart. -func LoadChartfile(filename string) (*Chartfile, error) { - b, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - var y Chartfile - return &y, yaml.Unmarshal(b, &y) -} - -// Save saves a Chart.yaml file -func (c *Chartfile) Save(filename string) error { - b, err := c.Marshal() - if err != nil { - return err - } - - return ioutil.WriteFile(filename, b, 0644) -} - -// Marshal encodes the chart file into YAML. -func (c *Chartfile) Marshal() ([]byte, error) { - return yaml.Marshal(c) -} - -// VersionOK returns true if the given version meets the constraints. -// -// It returns false if the version string or constraint is unparsable or if the -// version does not meet the constraint. -func (d *Dependency) VersionOK(version string) bool { - c, err := semver.NewConstraint(d.Version) - if err != nil { - return false - } - v, err := semver.NewVersion(version) - if err != nil { - return false - } - - return c.Check(v) -} diff --git a/pkg/chart/chartfile_test.go b/pkg/chart/chartfile_test.go deleted file mode 100644 index ac4237f83..000000000 --- a/pkg/chart/chartfile_test.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "testing" -) - -func TestLoadChartfile(t *testing.T) { - f, err := LoadChartfile(testfile) - if err != nil { - t.Errorf("Failed to open %s: %s", testfile, err) - return - } - - if len(f.Environment[0].Extensions) != 2 { - t.Errorf("Expected two extensions, got %d", len(f.Environment[0].Extensions)) - } - - if f.Name != "frobnitz" { - t.Errorf("Expected frobnitz, got %s", f.Name) - } - - if len(f.Maintainers) != 2 { - t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) - } - - if len(f.Dependencies) != 1 { - t.Errorf("Expected 2 dependencies, got %d", len(f.Dependencies)) - } - - if f.Dependencies[0].Name != "thingerbob" { - t.Errorf("Expected second dependency to be thingerbob: %q", f.Dependencies[0].Name) - } - - if f.Source[0] != "https://example.com/foo/bar" { - t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source) - } - - expander := f.Expander - if expander == nil { - t.Errorf("No expander found in %s", testfile) - } else { - if expander.Name != "expandybird-service" { - t.Errorf("Expected expander name expandybird-service, got %s", expander.Name) - } - - if expander.Entrypoint != "templates/wordpress.jinja" { - t.Errorf("Expected expander entrypoint templates/wordpress.jinja, got %s", expander.Entrypoint) - } - } - - if f.Schema != "wordpress.jinja.schema" { - t.Errorf("Expected schema wordpress.jinja.schema, got %s", f.Schema) - } -} - -func TestVersionOK(t *testing.T) { - f, err := LoadChartfile(testfile) - if err != nil { - t.Errorf("Error loading %s: %s", testfile, err) - } - - // These are canaries. The SemVer package exhuastively tests the - // various permutations. This will alert us if we wired it up - // incorrectly. - - d := f.Dependencies[0] - if d.VersionOK("1.0.0") { - t.Errorf("1.0.0 should have been marked out of range") - } - - if !d.VersionOK("3.2.3") { - t.Errorf("Version 3.2.3 should have been marked in-range") - } - -} diff --git a/pkg/chart/doc.go b/pkg/chart/doc.go deleted file mode 100644 index ec0627506..000000000 --- a/pkg/chart/doc.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -/* -Package chart implements the Chart format. - -This package provides tools for working with the Chart format, including the -Chartfile (chart.yaml) and compressed chart archives. -*/ -package chart diff --git a/pkg/chart/locator.go b/pkg/chart/locator.go deleted file mode 100644 index 749a83874..000000000 --- a/pkg/chart/locator.go +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "errors" - "fmt" - "net/url" - "regexp" - "strings" -) - -// ErrLocal indicates that a local URL was used as a remote URL. -var ErrLocal = errors.New("cannot use local Locator as remote") - -// ErrRemote indicates that a remote URL was used as a local URL. -var ErrRemote = errors.New("cannot use remote Locator as local") - -// Constants defining recognized URL schemes. -const ( - SchemeHTTP = "http" - SchemeHTTPS = "https" - SchemeGS = "gs" - SchemeS3 = "s3" - SchemeHelm = "helm" - SchemeFile = "file" -) - -// TarNameRegex parses the name component of a URI and breaks it into a name and version. -// -// This borrows liberally from github.com/Masterminds/semver. -const TarNameRegex = `([0-9A-Za-z\-_/]+)-(v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` + - `(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + - `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)(.tgz)?` - -var tnregexp *regexp.Regexp - -func init() { - tnregexp = regexp.MustCompile("^" + TarNameRegex + "$") -} - -// Locator describes the location of a Chart. -type Locator struct { - // The scheme of the URL. Typically one of http, https, helm, or file. - Scheme string - // The host information, if applicable. - Host string - // The bucket name - Bucket string - // The chart name - Name string - // The version or version range. - Version string - - // If this is a local chart, the path to the chart. - LocalRef string - - isLocal bool - - original string -} - -// Parse parses a URL into a Locator. -func Parse(path string) (*Locator, error) { - u, err := url.Parse(path) - if err != nil { - return nil, err - } - - switch u.Scheme { - case SchemeHelm: - parts := strings.SplitN(u.Opaque, "/", 3) - if len(parts) < 3 { - return nil, fmt.Errorf("both bucket and chart name are required in %s: %s", path, u.Path) - } - - // Need to parse opaque data into bucket and chart. - return &Locator{ - Scheme: u.Scheme, - Host: parts[0], - Bucket: parts[1], - Name: parts[2], - Version: u.Fragment, - original: path, - }, nil - - case SchemeHTTP, SchemeHTTPS: - // Long name - parts := strings.SplitN(u.Path, "/", 3) - if len(parts) < 3 { - return nil, fmt.Errorf("both bucket and chart name are required in %s", path) - } - - name, version, err := parseTarName(parts[2]) - if err != nil { - return nil, err - } - - return &Locator{ - Scheme: u.Scheme, - Host: u.Host, - Bucket: parts[1], - Name: name, - Version: version, - original: path, - }, nil - case SchemeGS, SchemeS3: - // Long name - parts := strings.SplitN(u.Path, "/", 2) - if len(parts) < 2 { - return nil, fmt.Errorf("chart name is required in %s", path) - } - - name, version, err := parseTarName(parts[1]) - if err != nil { - return nil, err - } - - return &Locator{ - Scheme: u.Scheme, - Host: u.Scheme, - Bucket: u.Host, - Name: name, - Version: version, - original: path, - }, nil - case SchemeFile: - return &Locator{ - LocalRef: u.Path, - isLocal: true, - original: path, - }, nil - default: - // In this case... - // - if the path is relative or absolute, return it as-is. - // - if it's a URL of an unknown scheme, return it as is. - return &Locator{ - LocalRef: path, - isLocal: true, - original: path, - }, nil - - } -} - -// IsLocal returns true if this is a local path. -func (u *Locator) IsLocal() bool { - return u.isLocal -} - -// Local returns a local version of the path. -// -// This will return an error if the URL does not reference a local chart. -func (u *Locator) Local() (string, error) { - return u.LocalRef, nil -} - -// Short returns a short form URL. -// -// This will return an error if the URL references a local chart. -func (u *Locator) Short() (string, error) { - if u.IsLocal() { - return "", ErrLocal - } - - fname := fmt.Sprintf("%s/%s/%s", u.Host, u.Bucket, u.Name) - return (&url.URL{ - Scheme: SchemeHelm, - Opaque: fname, - Fragment: u.Version, - }).String(), nil -} - -// Long returns a long-form URL. -// -// If secure is true, this will return an HTTPS URL, otherwise HTTP. -// -// This will return an error if the URL references a local chart. -func (u *Locator) Long(secure bool) (string, error) { - if u.IsLocal() { - return "", ErrLocal - } - - scheme := u.Scheme - host := u.Host - switch scheme { - case SchemeGS, SchemeS3: - host = "" - case SchemeHTTP, SchemeHTTPS, SchemeHelm: - switch host { - case SchemeGS, SchemeS3: - scheme = host - host = "" - default: - scheme = SchemeHTTPS - if !secure { - scheme = SchemeHTTP - } - } - } - - fname := fmt.Sprintf("%s/%s-%s.tgz", u.Bucket, u.Name, u.Version) - return (&url.URL{ - Scheme: scheme, - Host: host, - Path: fname, - }).String(), nil -} - -// parseTarName parses a long-form tarfile name. -func parseTarName(name string) (string, string, error) { - if strings.HasSuffix(name, ".tgz") { - name = strings.TrimSuffix(name, ".tgz") - } - v := tnregexp.FindStringSubmatch(name) - if v == nil { - return name, "", fmt.Errorf("invalid name %s", name) - } - return v[1], v[2], nil -} diff --git a/pkg/chart/locator_test.go b/pkg/chart/locator_test.go deleted file mode 100644 index dbafae114..000000000 --- a/pkg/chart/locator_test.go +++ /dev/null @@ -1,210 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "testing" -) - -func TestParse(t *testing.T) { - tests := map[string]Locator{ - "helm:host/bucket/name#1.2.3": {Scheme: "helm", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"}, - "https://host/bucket/name-1.2.3.tgz": {Scheme: "https", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"}, - "http://host/bucket/name-1.2.3.tgz": {Scheme: "http", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"}, - } - - for start, expect := range tests { - u, err := Parse(start) - if err != nil { - t.Errorf("Failed parsing %s: %s", start, err) - } - - if expect.Scheme != u.Scheme { - t.Errorf("Unexpected scheme: %s", u.Scheme) - } - - if expect.Host != u.Host { - t.Errorf("Unexpected host: %q", u.Host) - } - - if expect.Bucket != u.Bucket { - t.Errorf("Unexpected bucket: %q", u.Bucket) - } - - if expect.Name != u.Name { - t.Errorf("Unexpected name: %q", u.Name) - } - - if expect.Version != u.Version { - t.Errorf("Unexpected version: %q", u.Version) - } - - if expect.LocalRef != u.LocalRef { - t.Errorf("Unexpected local dir: %q", u.LocalRef) - } - - } -} - -func TestShort(t *testing.T) { - tests := map[string]string{ - "https://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3", - "http://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3", - "gs://foo/bar-1.2.3.tgz": "helm:gs/foo/bar#1.2.3", - "s3://foo/bar-1.2.3.tgz": "helm:s3/foo/bar#1.2.3", - "helm:example.com/foo/bar#1.2.3": "helm:example.com/foo/bar#1.2.3", - "helm:example.com/foo/bar#>1.2.3": "helm:example.com/foo/bar#%3E1.2.3", - "helm:gs/foo/bar#1.2.3": "helm:gs/foo/bar#1.2.3", - "helm:s3/foo/bar#>1.2.3": "helm:s3/foo/bar#%3E1.2.3", - } - - for start, expect := range tests { - u, err := Parse(start) - if err != nil { - t.Errorf("Failed to parse: %s", err) - continue - } - - t.Logf("Parsed reference %s into locator %#v", start, u) - - short, err := u.Short() - if err != nil { - t.Errorf("Failed to generate short: %s", err) - continue - } - - if short != expect { - t.Errorf("Expected %q, got %q", expect, short) - } - } - - fails := []string{"./this/is/local", "file:///this/is/local"} - for _, f := range fails { - u, err := Parse(f) - if err != nil { - t.Errorf("Failed to parse: %s", err) - continue - } - - if _, err := u.Short(); err == nil { - t.Errorf("%q should have caused an error for Short()", f) - } - } -} - -func TestLong(t *testing.T) { - tests := map[string]string{ - "https://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz", - "http://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz", - "gs://foo/bar-1.2.3.tgz": "gs://foo/bar-1.2.3.tgz", - "s3://foo/bar-1.2.3.tgz": "s3://foo/bar-1.2.3.tgz", - "helm:example.com/foo/bar#1.2.3": "https://example.com/foo/bar-1.2.3.tgz", - "helm:example.com/foo/bar#>1.2.3": "https://example.com/foo/bar-%3E1.2.3.tgz", - "helm:gs/foo/bar#1.2.3": "gs://foo/bar-1.2.3.tgz", - "helm:s3/foo/bar#>1.2.3": "s3://foo/bar-%3E1.2.3.tgz", - } - - for start, expect := range tests { - t.Logf("Parsing %s", start) - u, err := Parse(start) - if err != nil { - t.Errorf("Failed to parse: %s", err) - continue - } - - t.Logf("Parsed reference %s into locator %#v", start, u) - - long, err := u.Long(true) - if err != nil { - t.Errorf("Failed to generate long: %s", err) - continue - } - - if long != expect { - t.Errorf("Expected %q, got %q", expect, long) - } - } - - fails := []string{"./this/is/local", "file:///this/is/local"} - for _, f := range fails { - u, err := Parse(f) - if err != nil { - t.Errorf("Failed to parse: %s", err) - continue - } - - if _, err := u.Long(false); err == nil { - t.Errorf("%q should have caused an error for Long()", f) - } - } -} - -func TestLocal(t *testing.T) { - tests := map[string]string{ - "file:///foo/bar-1.2.3.tgz": "/foo/bar-1.2.3.tgz", - "file:///foo/bar": "/foo/bar", - "./foo/bar": "./foo/bar", - "/foo/bar": "/foo/bar", - "file://localhost/etc/fstab": "/etc/fstab", - // https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/ - "file:///C:/WINDOWS/clock.avi": "/C:/WINDOWS/clock.avi", - } - - for start, expect := range tests { - u, err := Parse(start) - if err != nil { - t.Errorf("Failed parse: %s", err) - continue - } - - fin, err := u.Local() - if err != nil { - t.Errorf("Failed Local(): %s", err) - continue - } - - if fin != expect { - t.Errorf("Expected %q, got %q", expect, fin) - } - } - -} - -func TestParseTarName(t *testing.T) { - tests := []struct{ start, name, version string }{ - {"butcher-1.2.3", "butcher", "1.2.3"}, - {"butcher-1.2.3.tgz", "butcher", "1.2.3"}, - {"butcher-1.2.3-beta1+1234", "butcher", "1.2.3-beta1+1234"}, - {"butcher-1.2.3-beta1+1234.tgz", "butcher", "1.2.3-beta1+1234"}, - {"foo/butcher-1.2.3.tgz", "foo/butcher", "1.2.3"}, - } - - for _, tt := range tests { - n, v, e := parseTarName(tt.start) - if e != nil { - t.Errorf("Error parsing %s: %s", tt.start, e) - continue - } - if n != tt.name { - t.Errorf("Expected name %q, got %q", tt.name, n) - } - - if v != tt.version { - t.Errorf("Expected version %q, got %q", tt.version, v) - } - } -} diff --git a/pkg/chart/save.go b/pkg/chart/save.go deleted file mode 100644 index d9d0133d6..000000000 --- a/pkg/chart/save.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "archive/tar" - "compress/gzip" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/kubernetes/helm/pkg/log" -) - -// Save creates an archived chart to the given directory. -// -// This takes an existing chart and a destination directory. -// -// If the directory is /foo, and the chart is named bar, with version 1.0.0, this -// will generate /foo/bar-1.0.0.tgz. -// -// This returns the absolute path to the chart archive file. -func Save(c *Chart, outDir string) (string, error) { - // Create archive - if fi, err := os.Stat(outDir); err != nil { - return "", err - } else if !fi.IsDir() { - return "", fmt.Errorf("location %s is not a directory", outDir) - } - - cfile := c.Chartfile() - dir := c.Dir() - pdir := filepath.Dir(dir) - filename := fmt.Sprintf("%s-%s.tgz", fname(cfile.Name), cfile.Version) - filename = filepath.Join(outDir, filename) - - // Fail early if the YAML is borked. - if err := cfile.Save(filepath.Join(dir, ChartfileName)); err != nil { - return "", err - } - - // Create file. - f, err := os.Create(filename) - if err != nil { - return "", err - } - - // Wrap in gzip writer - zipper := gzip.NewWriter(f) - zipper.Header.Extra = headerBytes - zipper.Header.Comment = "Helm" - - // Wrap in tar writer - twriter := tar.NewWriter(zipper) - rollback := false - defer func() { - twriter.Close() - zipper.Close() - f.Close() - if rollback { - log.Warn("Removing incomplete archive %s", filename) - os.Remove(filename) - } - }() - - err = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { - if err != nil { - return err - } - hdr, err := tar.FileInfoHeader(fi, ".") - if err != nil { - return err - } - - relpath, err := filepath.Rel(pdir, path) - if err != nil { - return err - } - hdr.Name = relpath - - twriter.WriteHeader(hdr) - - // Skip directories. - if fi.IsDir() { - return nil - } - - in, err := os.Open(path) - if err != nil { - return err - } - _, err = io.Copy(twriter, in) - in.Close() - if err != nil { - return err - } - - return nil - }) - if err != nil { - rollback = true - return filename, err - } - return filename, nil -} diff --git a/pkg/chart/save_test.go b/pkg/chart/save_test.go deleted file mode 100644 index 56979c6a1..000000000 --- a/pkg/chart/save_test.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package chart - -import ( - "archive/tar" - "compress/gzip" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -const sprocketdir = "testdata/sprocket" - -func TestSave(t *testing.T) { - - tmpdir, err := ioutil.TempDir("", "helm-") - if err != nil { - t.Fatal("Could not create temp directory") - } - t.Logf("Temp: %s", tmpdir) - // Because of the defer, don't call t.Fatal in the remainder of this - // function. - defer os.RemoveAll(tmpdir) - - c, err := LoadDir(sprocketdir) - if err != nil { - t.Errorf("Failed to load %s: %s", sprocketdir, err) - return - } - - tfile, err := Save(c, tmpdir) - if err != nil { - t.Errorf("Failed to save %s to %s: %s", c.Chartfile().Name, tmpdir, err) - return - } - - b := filepath.Base(tfile) - expectname := "sprocket-1.2.3-alpha.1+12345.tgz" - if b != expectname { - t.Errorf("Expected %q, got %q", expectname, b) - } - - files, err := getAllFiles(tfile) - if err != nil { - t.Errorf("Could not extract files: %s", err) - } - - // Files should come back in order. - expect := []string{ - "sprocket", - "sprocket/Chart.yaml", - "sprocket/LICENSE", - "sprocket/README.md", - "sprocket/docs", - "sprocket/docs/README.md", - "sprocket/hooks", - "sprocket/hooks/pre-install.py", - "sprocket/icon.svg", - "sprocket/templates", - "sprocket/templates/placeholder.txt", - } - if len(expect) != len(files) { - t.Errorf("Expected %d files, found %d", len(expect), len(files)) - return - } - for i := 0; i < len(expect); i++ { - if expect[i] != files[i] { - t.Errorf("Expected file %q, got %q", expect[i], files[i]) - } - } -} - -func getAllFiles(tfile string) ([]string, error) { - f1, err := os.Open(tfile) - if err != nil { - return []string{}, err - } - f2, err := gzip.NewReader(f1) - if err != nil { - f1.Close() - return []string{}, err - } - - if f2.Header.Comment != "Helm" { - return []string{}, fmt.Errorf("Expected header Helm. Got %s", f2.Header.Comment) - } - if string(f2.Header.Extra) != string(headerBytes) { - return []string{}, fmt.Errorf("Expected header signature. Got %v", f2.Header.Extra) - } - - f3 := tar.NewReader(f2) - - files := []string{} - var e error - var hdr *tar.Header - for e == nil { - hdr, e = f3.Next() - if e == nil { - files = append(files, hdr.Name) - } - } - - f2.Close() - f1.Close() - return files, nil -} diff --git a/pkg/chart/testdata/README.md b/pkg/chart/testdata/README.md deleted file mode 100644 index 6ddc704f5..000000000 --- a/pkg/chart/testdata/README.md +++ /dev/null @@ -1,9 +0,0 @@ -The testdata directory here holds charts that match the specification. - -The `fromnitz/` directory contains a chart that matches the chart -specification. - -The `frobnitz-0.0.1.tgz` file is an archive of the `frobnitz` directory. - -The `ill` chart and directory is a chart that is not 100% compatible, -but which should still be parseable. diff --git a/pkg/chart/testdata/frobnitz-0.0.1.tgz b/pkg/chart/testdata/frobnitz-0.0.1.tgz deleted file mode 100644 index 3c3a7cf010057a05819e46a9525225f00231cdc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2382 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 A(*OVf diff --git a/pkg/chart/testdata/frobnitz/Chart.yaml b/pkg/chart/testdata/frobnitz/Chart.yaml deleted file mode 100644 index fd5754b56..000000000 --- a/pkg/chart/testdata/frobnitz/Chart.yaml +++ /dev/null @@ -1,33 +0,0 @@ -#helm:generate foo -name: frobnitz -description: This is a frobniz. -version: "1.2.3-alpha.1+12345" -keywords: - - frobnitz - - sprocket - - dodad -maintainers: - - name: The Helm Team - email: helm@example.com - - name: Someone Else - email: nobody@example.com -source: - - https://example.com/foo/bar -home: http://example.com -dependencies: - - name: thingerbob - location: https://example.com/charts/thingerbob-3.2.1.tgz - version: ^3 -environment: - - name: Kubernetes - version: ~1.1 - extensions: - - extensions/v1beta1 - - extensions/v1beta1/daemonset - apiGroups: - - 3rdParty -expander: - name: expandybird-service - entrypoint: templates/wordpress.jinja -schema: wordpress.jinja.schema - \ No newline at end of file diff --git a/pkg/chart/testdata/frobnitz/LICENSE b/pkg/chart/testdata/frobnitz/LICENSE deleted file mode 100644 index 6121943b1..000000000 --- a/pkg/chart/testdata/frobnitz/LICENSE +++ /dev/null @@ -1 +0,0 @@ -LICENSE placeholder. diff --git a/pkg/chart/testdata/frobnitz/README.md b/pkg/chart/testdata/frobnitz/README.md deleted file mode 100644 index 8cf4cc3d7..000000000 --- a/pkg/chart/testdata/frobnitz/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Frobnitz - -This is an example chart. - -## Usage - -This is an example. It has no usage. - -## Development - -For developer info, see the top-level repository. diff --git a/pkg/chart/testdata/frobnitz/docs/README.md b/pkg/chart/testdata/frobnitz/docs/README.md deleted file mode 100644 index d40747caf..000000000 --- a/pkg/chart/testdata/frobnitz/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a placeholder for documentation. diff --git a/pkg/chart/testdata/frobnitz/hooks/pre-install.py b/pkg/chart/testdata/frobnitz/hooks/pre-install.py deleted file mode 100644 index c9b0d0a92..000000000 --- a/pkg/chart/testdata/frobnitz/hooks/pre-install.py +++ /dev/null @@ -1 +0,0 @@ -# Placeholder. diff --git a/pkg/chart/testdata/frobnitz/icon.svg b/pkg/chart/testdata/frobnitz/icon.svg deleted file mode 100644 index 892130606..000000000 --- a/pkg/chart/testdata/frobnitz/icon.svg +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0"?> -<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - version="1.0" width="256" height="256" id="test"> - <desc>Example icon</desc> - <rect id="first" x="2" y="2" width="40" height="60" fill="navy"/> - <rect id="second" x="15" y="4" width="40" height="60" fill="red"/> -</svg> diff --git a/pkg/chart/testdata/frobnitz/templates/wordpress-resources.yaml b/pkg/chart/testdata/frobnitz/templates/wordpress-resources.yaml deleted file mode 100644 index 00f709de0..000000000 --- a/pkg/chart/testdata/frobnitz/templates/wordpress-resources.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Google Cloud Deployment Manager template -resources: -- name: nfs-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 -- name: mysql-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 diff --git a/pkg/chart/testdata/frobnitz/templates/wordpress.jinja b/pkg/chart/testdata/frobnitz/templates/wordpress.jinja deleted file mode 100644 index f34e4fec9..000000000 --- a/pkg/chart/testdata/frobnitz/templates/wordpress.jinja +++ /dev/null @@ -1,72 +0,0 @@ -#helm:generate dm_template -{% set PROPERTIES = properties or {} %} -{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %} -{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %} -{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %} -{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %} -{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %} -{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %} -{% set NGINX = PROPERTIES['nginx'] or {} %} -{% set NGINX_PORT = 80 %} -{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %} -{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %} -{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %} -{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %} -{% set MYSQL = PROPERTIES['mysql'] or {} %} -{% set MYSQL_PORT = MYSQL['port'] or 3306 %} -{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %} -{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %} -{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %} - -resources: -- name: nfs - type: github.com/kubernetes/application-dm-templates/storage/nfs:v1 - properties: - ip: {{ NFS_SERVER_IP }} - port: {{ NFS_SERVER_PORT }} - disk: {{ NFS_SERVER_DISK }} - fstype: {{NFS_SERVER_DISK_FSTYPE }} -- name: nginx - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_port: {{ NGINX_PORT }} - container_port: {{ NGINX_PORT }} - replicas: {{ NGINX_REPLICAS }} - external_service: true - image: gcr.io/{{ PROJECT }}/nginx:latest - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs -- name: mysql - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_port: {{ MYSQL_PORT }} - container_port: {{ MYSQL_PORT }} - replicas: 1 - image: mysql:5.6 - env: - - name: MYSQL_ROOT_PASSWORD - value: {{ MYSQL_PASSWORD }} - volumes: - - mount_path: /var/lib/mysql - gcePersistentDisk: - pdName: {{ MYSQL_DISK }} - fsType: {{ MYSQL_DISK_FSTYPE }} -- name: wordpress-php - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_name: wordpress-php - service_port: {{ WORDPRESS_PHP_PORT }} - container_port: {{ WORDPRESS_PHP_PORT }} - replicas: 2 - image: wordpress:fpm - env: - - name: WORDPRESS_DB_PASSWORD - value: {{ MYSQL_PASSWORD }} - - name: WORDPRESS_DB_HOST - value: mysql-service - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs diff --git a/pkg/chart/testdata/frobnitz/templates/wordpress.jinja.schema b/pkg/chart/testdata/frobnitz/templates/wordpress.jinja.schema deleted file mode 100644 index 215b47e1e..000000000 --- a/pkg/chart/testdata/frobnitz/templates/wordpress.jinja.schema +++ /dev/null @@ -1,69 +0,0 @@ -info: - title: Wordpress - description: | - Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service. - - The nginx service and the Wordpress-php service both use NFS to share files. - -properties: - project: - type: string - default: dm-k8s-testing - description: Project location to load the images from. - nfs-service: - type: object - properties: - ip: - type: string - default: 10.0.253.247 - description: The IP of the NFS service. - port: - type: int - default: 2049 - description: The port of the NFS service. - disk: - type: string - default: nfs-disk - description: The name of the persistent disk the NFS service uses. - fstype: - type: string - default: ext4 - description: The filesystem the disk of the NFS service uses. - nginx: - type: object - properties: - replicas: - type: int - default: 2 - description: The number of replicas for the nginx service. - wordpress-php: - type: object - properties: - replicas: - type: int - default: 2 - description: The number of replicas for the wordpress-php service. - port: - type: int - default: 9000 - description: The port the wordpress-php service runs on. - mysql: - type: object - properties: - port: - type: int - default: 3306 - description: The port the MySQL service runs on. - password: - type: string - default: mysql-password - description: The root password of the MySQL service. - disk: - type: string - default: mysql-disk - description: The name of the persistent disk the MySQL service uses. - fstype: - type: string - default: ext4 - description: The filesystem the disk of the MySQL service uses. - diff --git a/pkg/chart/testdata/frobnitz/templates/wordpress.yaml b/pkg/chart/testdata/frobnitz/templates/wordpress.yaml deleted file mode 100644 index b401897ab..000000000 --- a/pkg/chart/testdata/frobnitz/templates/wordpress.yaml +++ /dev/null @@ -1,6 +0,0 @@ -imports: -- path: wordpress.jinja - -resources: -- name: wordpress - type: wordpress.jinja diff --git a/pkg/chart/testdata/ill-1.2.3.tgz b/pkg/chart/testdata/ill-1.2.3.tgz deleted file mode 100644 index 198cd2e877f732eaf3068390d3022319df2819ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2305 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 b&_M?sbkIQu9dyva7sLMmW7UnI08jt`g&BET diff --git a/pkg/chart/testdata/ill/Chart.yaml b/pkg/chart/testdata/ill/Chart.yaml deleted file mode 100644 index 1256cea32..000000000 --- a/pkg/chart/testdata/ill/Chart.yaml +++ /dev/null @@ -1,28 +0,0 @@ -#helm:generate foo -name: ill -description: This is a frobniz. -version: "1.2.3-alpha.1+12345" -keywords: - - ill - - sprocket - - dodad -maintainers: - - name: The Helm Team - email: helm@example.com - - name: Someone Else - email: nobody@example.com -source: - - https://example.com/foo/bar -home: http://example.com -dependencies: - - name: thingerbob - location: https://example.com/charts/thingerbob-3.2.1.tgz - version: ^3 -environment: - - name: Kubernetes - version: ~1.1 - extensions: - - extensions/v1beta1 - - extensions/v1beta1/daemonset - apiGroups: - - 3rdParty diff --git a/pkg/chart/testdata/ill/LICENSE b/pkg/chart/testdata/ill/LICENSE deleted file mode 100644 index 6121943b1..000000000 --- a/pkg/chart/testdata/ill/LICENSE +++ /dev/null @@ -1 +0,0 @@ -LICENSE placeholder. diff --git a/pkg/chart/testdata/ill/README.md b/pkg/chart/testdata/ill/README.md deleted file mode 100644 index 8cf4cc3d7..000000000 --- a/pkg/chart/testdata/ill/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Frobnitz - -This is an example chart. - -## Usage - -This is an example. It has no usage. - -## Development - -For developer info, see the top-level repository. diff --git a/pkg/chart/testdata/ill/docs/README.md b/pkg/chart/testdata/ill/docs/README.md deleted file mode 100644 index d40747caf..000000000 --- a/pkg/chart/testdata/ill/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a placeholder for documentation. diff --git a/pkg/chart/testdata/ill/hooks/pre-install.py b/pkg/chart/testdata/ill/hooks/pre-install.py deleted file mode 100644 index c9b0d0a92..000000000 --- a/pkg/chart/testdata/ill/hooks/pre-install.py +++ /dev/null @@ -1 +0,0 @@ -# Placeholder. diff --git a/pkg/chart/testdata/ill/templates/wordpress-resources.yaml b/pkg/chart/testdata/ill/templates/wordpress-resources.yaml deleted file mode 100644 index 00f709de0..000000000 --- a/pkg/chart/testdata/ill/templates/wordpress-resources.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Google Cloud Deployment Manager template -resources: -- name: nfs-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 -- name: mysql-disk - type: compute.v1.disk - properties: - zone: us-central1-b - sizeGb: 200 diff --git a/pkg/chart/testdata/ill/templates/wordpress.jinja b/pkg/chart/testdata/ill/templates/wordpress.jinja deleted file mode 100644 index f34e4fec9..000000000 --- a/pkg/chart/testdata/ill/templates/wordpress.jinja +++ /dev/null @@ -1,72 +0,0 @@ -#helm:generate dm_template -{% set PROPERTIES = properties or {} %} -{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %} -{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %} -{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %} -{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %} -{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %} -{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %} -{% set NGINX = PROPERTIES['nginx'] or {} %} -{% set NGINX_PORT = 80 %} -{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %} -{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %} -{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %} -{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %} -{% set MYSQL = PROPERTIES['mysql'] or {} %} -{% set MYSQL_PORT = MYSQL['port'] or 3306 %} -{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %} -{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %} -{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %} - -resources: -- name: nfs - type: github.com/kubernetes/application-dm-templates/storage/nfs:v1 - properties: - ip: {{ NFS_SERVER_IP }} - port: {{ NFS_SERVER_PORT }} - disk: {{ NFS_SERVER_DISK }} - fstype: {{NFS_SERVER_DISK_FSTYPE }} -- name: nginx - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_port: {{ NGINX_PORT }} - container_port: {{ NGINX_PORT }} - replicas: {{ NGINX_REPLICAS }} - external_service: true - image: gcr.io/{{ PROJECT }}/nginx:latest - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs -- name: mysql - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_port: {{ MYSQL_PORT }} - container_port: {{ MYSQL_PORT }} - replicas: 1 - image: mysql:5.6 - env: - - name: MYSQL_ROOT_PASSWORD - value: {{ MYSQL_PASSWORD }} - volumes: - - mount_path: /var/lib/mysql - gcePersistentDisk: - pdName: {{ MYSQL_DISK }} - fsType: {{ MYSQL_DISK_FSTYPE }} -- name: wordpress-php - type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 - properties: - service_name: wordpress-php - service_port: {{ WORDPRESS_PHP_PORT }} - container_port: {{ WORDPRESS_PHP_PORT }} - replicas: 2 - image: wordpress:fpm - env: - - name: WORDPRESS_DB_PASSWORD - value: {{ MYSQL_PASSWORD }} - - name: WORDPRESS_DB_HOST - value: mysql-service - volumes: - - mount_path: /var/www/html - persistentVolumeClaim: - claimName: nfs diff --git a/pkg/chart/testdata/ill/templates/wordpress.jinja.schema b/pkg/chart/testdata/ill/templates/wordpress.jinja.schema deleted file mode 100644 index 215b47e1e..000000000 --- a/pkg/chart/testdata/ill/templates/wordpress.jinja.schema +++ /dev/null @@ -1,69 +0,0 @@ -info: - title: Wordpress - description: | - Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service. - - The nginx service and the Wordpress-php service both use NFS to share files. - -properties: - project: - type: string - default: dm-k8s-testing - description: Project location to load the images from. - nfs-service: - type: object - properties: - ip: - type: string - default: 10.0.253.247 - description: The IP of the NFS service. - port: - type: int - default: 2049 - description: The port of the NFS service. - disk: - type: string - default: nfs-disk - description: The name of the persistent disk the NFS service uses. - fstype: - type: string - default: ext4 - description: The filesystem the disk of the NFS service uses. - nginx: - type: object - properties: - replicas: - type: int - default: 2 - description: The number of replicas for the nginx service. - wordpress-php: - type: object - properties: - replicas: - type: int - default: 2 - description: The number of replicas for the wordpress-php service. - port: - type: int - default: 9000 - description: The port the wordpress-php service runs on. - mysql: - type: object - properties: - port: - type: int - default: 3306 - description: The port the MySQL service runs on. - password: - type: string - default: mysql-password - description: The root password of the MySQL service. - disk: - type: string - default: mysql-disk - description: The name of the persistent disk the MySQL service uses. - fstype: - type: string - default: ext4 - description: The filesystem the disk of the MySQL service uses. - diff --git a/pkg/chart/testdata/ill/templates/wordpress.yaml b/pkg/chart/testdata/ill/templates/wordpress.yaml deleted file mode 100644 index b401897ab..000000000 --- a/pkg/chart/testdata/ill/templates/wordpress.yaml +++ /dev/null @@ -1,6 +0,0 @@ -imports: -- path: wordpress.jinja - -resources: -- name: wordpress - type: wordpress.jinja diff --git a/pkg/chart/testdata/nochart.tgz b/pkg/chart/testdata/nochart.tgz deleted file mode 100644 index f5a235537aa99fd1b58faec20e3e3e871f5b2ab1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 325 zcmV-L0lNMliwFP@XsT8K1MSj5PlGTR2H>6f72n01Vqt@wm$;)B;}4*eVw*s_wBI!D z*V{2&h*58gF}}~Glze%43p{K$clX<T>=u_Jgs4|5I<D)wnpRPs_LHhuQ&HAcSyszh zlq8CkSc(OS3wAo&N0uQ~86|VOXJvHwaxRVa{SP^(qtJQ%BOZ+=){#5Ok~$l-Vjo^9 zy#-Ci4_zc>WZ+0_BvX1x%A`w8ai^8-t&(iL%d_m5)AYfQ&hp{5nMEn@SXviJo}y++ zI!;;B?d-e6_Lz7ha*-D~KOGwC?V!n~JV!DpW1ltoehNB|&z}F0WQU=7FCbkv>t{bT zOMB;;3L&mi_i>l{w1d9R($7IJ#-{T}<M%^Hky9qMnszcoEz?k*{r2Ys0000000000 X0000000000fPdr-TvP;704M+e69$!? diff --git a/pkg/chart/testdata/sprocket/Chart.yaml b/pkg/chart/testdata/sprocket/Chart.yaml deleted file mode 100644 index 771ea87cb..000000000 --- a/pkg/chart/testdata/sprocket/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: sprocket -description: This is a sprocket. -version: 1.2.3-alpha.1+12345 -home: "" diff --git a/pkg/chart/testdata/sprocket/LICENSE b/pkg/chart/testdata/sprocket/LICENSE deleted file mode 100644 index 6121943b1..000000000 --- a/pkg/chart/testdata/sprocket/LICENSE +++ /dev/null @@ -1 +0,0 @@ -LICENSE placeholder. diff --git a/pkg/chart/testdata/sprocket/README.md b/pkg/chart/testdata/sprocket/README.md deleted file mode 100644 index d1daac8da..000000000 --- a/pkg/chart/testdata/sprocket/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sprocket - -This is an example chart. diff --git a/pkg/chart/testdata/sprocket/docs/README.md b/pkg/chart/testdata/sprocket/docs/README.md deleted file mode 100644 index d40747caf..000000000 --- a/pkg/chart/testdata/sprocket/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a placeholder for documentation. diff --git a/pkg/chart/testdata/sprocket/hooks/pre-install.py b/pkg/chart/testdata/sprocket/hooks/pre-install.py deleted file mode 100644 index c9b0d0a92..000000000 --- a/pkg/chart/testdata/sprocket/hooks/pre-install.py +++ /dev/null @@ -1 +0,0 @@ -# Placeholder. diff --git a/pkg/chart/testdata/sprocket/icon.svg b/pkg/chart/testdata/sprocket/icon.svg deleted file mode 100644 index 892130606..000000000 --- a/pkg/chart/testdata/sprocket/icon.svg +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0"?> -<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - version="1.0" width="256" height="256" id="test"> - <desc>Example icon</desc> - <rect id="first" x="2" y="2" width="40" height="60" fill="navy"/> - <rect id="second" x="15" y="4" width="40" height="60" fill="red"/> -</svg> diff --git a/pkg/chart/testdata/sprocket/templates/placeholder.txt b/pkg/chart/testdata/sprocket/templates/placeholder.txt deleted file mode 100644 index ef9fb20a7..000000000 --- a/pkg/chart/testdata/sprocket/templates/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -This is a placeholder. diff --git a/pkg/client/client.go b/pkg/client/client.go deleted file mode 100644 index a3e4d5b25..000000000 --- a/pkg/client/client.go +++ /dev/null @@ -1,282 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "strings" - "time" - - "github.com/kubernetes/helm/pkg/httputil" - "github.com/kubernetes/helm/pkg/version" -) - -const ( - // DefaultHTTPTimeout is the default HTTP timeout. - DefaultHTTPTimeout = time.Second * 10 - // DefaultHTTPProtocol is the default HTTP Protocol (http, https). - DefaultHTTPProtocol = "http" -) - -// Client is a DM client. -type Client struct { - // Timeout on HTTP connections. - HTTPTimeout time.Duration - // Transport - Transport http.RoundTripper - // Debug enables http logging - Debug bool - // Base URL for remote service - baseURL *url.URL -} - -// NewClient creates a new DM client. Host name is required. -func NewClient(host string) *Client { - url, _ := DefaultServerURL(host) - - return &Client{ - HTTPTimeout: DefaultHTTPTimeout, - baseURL: url, - Transport: http.DefaultTransport, - } -} - -// SetDebug enables debug mode which logs http -func (c *Client) SetDebug(enable bool) *Client { - c.Debug = enable - return c -} - -// transport wraps client transport if debug is enabled -func (c *Client) transport() http.RoundTripper { - if c.Debug { - return NewDebugTransport(c.Transport) - } - return c.Transport -} - -// SetTransport sets a custom Transport. Defaults to http.DefaultTransport -func (c *Client) SetTransport(tr http.RoundTripper) *Client { - c.Transport = tr - return c -} - -// SetTimeout sets a timeout for http connections -func (c *Client) SetTimeout(seconds int) *Client { - c.HTTPTimeout = time.Duration(seconds) * time.Second - return c -} - -// url constructs the URL. -func (c *Client) url(rawurl string) (string, error) { - u, err := url.Parse(rawurl) - if err != nil { - return "", err - } - return c.baseURL.ResolveReference(u).String(), nil -} - -func (c *Client) agent() string { - return fmt.Sprintf("helm/%s", version.Version) -} - -// Get calls GET on an endpoint and decodes the response -func (c *Client) Get(endpoint string, v interface{}) (*Response, error) { - return c.Exec(c.NewRequest("GET", endpoint, nil), &v) -} - -// Post calls POST on an endpoint and decodes the response -func (c *Client) Post(endpoint string, payload, v interface{}) (*Response, error) { - return c.Exec(c.NewRequest("POST", endpoint, payload), &v) -} - -// Delete calls DELETE on an endpoint and decodes the response -func (c *Client) Delete(endpoint string, v interface{}) (*Response, error) { - return c.Exec(c.NewRequest("DELETE", endpoint, nil), &v) -} - -// NewRequest creates a new client request -func (c *Client) NewRequest(method, endpoint string, payload interface{}) *Request { - u, err := c.url(endpoint) - if err != nil { - return &Request{error: err} - } - - body := prepareBody(payload) - req, err := http.NewRequest(method, u, body) - - req.Header.Set("User-Agent", c.agent()) - req.Header.Set("Accept", "application/json") - - // TODO: set Content-Type based on body - req.Header.Add("Content-Type", "application/json") - - return &Request{req, err} -} - -func prepareBody(payload interface{}) io.Reader { - var body io.Reader - switch t := payload.(type) { - default: - //FIXME: panic is only for development - panic(fmt.Sprintf("unexpected type %T\n", t)) - case io.Reader: - body = t - case []byte: - body = bytes.NewBuffer(t) - case nil: - } - return body -} - -// Exec sends a request and decodes the response -func (c *Client) Exec(req *Request, v interface{}) (*Response, error) { - return c.Result(c.Do(req), &v) -} - -// Result checks status code and decodes a response body -func (c *Client) Result(resp *Response, v interface{}) (*Response, error) { - switch { - case resp.error != nil: - return resp, resp - case !resp.Success(): - return resp, resp.HTTPError() - } - return resp, decodeResponse(resp, v) -} - -// Do send a request and returns a response -func (c *Client) Do(req *Request) *Response { - if req.error != nil { - return &Response{error: req} - } - - client := &http.Client{ - Timeout: c.HTTPTimeout, - Transport: c.transport(), - } - resp, err := client.Do(req.Request) - return &Response{resp, err} -} - -// DefaultServerURL converts a host, host:port, or URL string to the default base server API path -// to use with a Client -func DefaultServerURL(host string) (*url.URL, error) { - if host == "" { - return nil, fmt.Errorf("host must be a URL or a host:port pair") - } - base := host - hostURL, err := url.Parse(base) - if err != nil { - return nil, err - } - if hostURL.Scheme == "" { - hostURL, err = url.Parse(DefaultHTTPProtocol + "://" + base) - if err != nil { - return nil, err - } - } - if len(hostURL.Path) > 0 && !strings.HasSuffix(hostURL.Path, "/") { - hostURL.Path = hostURL.Path + "/" - } - - return hostURL, nil -} - -// Request wraps http.Request to include error -type Request struct { - *http.Request - error -} - -// Response wraps http.Response to include error -type Response struct { - *http.Response - error -} - -// Success returns true if the status code is 2xx -func (r *Response) Success() bool { - return r.StatusCode >= 200 && r.StatusCode < 300 -} - -// HTTPError creates a new HTTPError from response -func (r *Response) HTTPError() error { - contentType := r.Header.Get("Content-Type") - - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - if contentType == "application/json" { - httpErr := httputil.Error{} - if err := json.Unmarshal(body, &httpErr); err != nil { - return err - } - return &HTTPError{ - StatusCode: r.StatusCode, - Message: httpErr.Msg, - URL: r.Request.URL, - } - } - - return &HTTPError{ - StatusCode: r.StatusCode, - Message: string(body), - URL: r.Request.URL, - } - -} - -// HTTPError is an error caused by an unexpected HTTP status code. -// -// The StatusCode will not necessarily be a 4xx or 5xx. Any unexpected code -// may be returned. -type HTTPError struct { - StatusCode int - Message string - URL *url.URL -} - -// Error implements the error interface. -func (e *HTTPError) Error() string { - return e.Message -} - -// String implmenets the io.Stringer interface. -func (e *HTTPError) String() string { - return e.Error() -} - -func decodeResponse(resp *Response, v interface{}) error { - defer resp.Body.Close() - if resp.Body == nil { - return nil - } - if err := json.NewDecoder(resp.Body).Decode(v); err != nil { - return fmt.Errorf("Failed to parse JSON response from service") - } - return nil -} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go deleted file mode 100644 index f6e48ac11..000000000 --- a/pkg/client/client_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestDefaultServerURL(t *testing.T) { - tt := []struct { - host string - url string - }{ - {"127.0.0.1", "http://127.0.0.1"}, - {"127.0.0.1:8080", "http://127.0.0.1:8080"}, - {"foo.bar.com", "http://foo.bar.com"}, - {"foo.bar.com/prefix", "http://foo.bar.com/prefix/"}, - {"http://host/prefix", "http://host/prefix/"}, - {"https://host/prefix", "https://host/prefix/"}, - {"http://host", "http://host"}, - {"http://host/other", "http://host/other/"}, - } - - for _, tc := range tt { - u, err := DefaultServerURL(tc.host) - if err != nil { - t.Fatal(err) - } - - if tc.url != u.String() { - t.Errorf("%s, expected host %s, got %s", tc.host, tc.url, u.String()) - } - } -} - -func TestURL(t *testing.T) { - tt := []struct { - host string - path string - url string - }{ - {"127.0.0.1", "foo", "http://127.0.0.1/foo"}, - {"127.0.0.1:8080", "foo", "http://127.0.0.1:8080/foo"}, - {"foo.bar.com", "foo", "http://foo.bar.com/foo"}, - {"foo.bar.com/prefix", "foo", "http://foo.bar.com/prefix/foo"}, - {"http://host/prefix", "foo", "http://host/prefix/foo"}, - {"http://host", "foo", "http://host/foo"}, - {"http://host/other", "/foo", "http://host/foo"}, - } - - for _, tc := range tt { - c := NewClient(tc.host) - p, err := c.url(tc.path) - if err != nil { - t.Fatal(err) - } - - if tc.url != p { - t.Errorf("expected %s, got %s", tc.url, p) - } - } -} - -type fakeClient struct { - *Client - server *httptest.Server - handler http.HandlerFunc -} - -func (c *fakeClient) setup() *fakeClient { - c.server = httptest.NewServer(c.handler) - c.Client = NewClient(c.server.URL) - return c -} - -func (c *fakeClient) teardown() { - c.server.Close() -} - -func TestUserAgent(t *testing.T) { - fc := &fakeClient{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.UserAgent(), "helm") { - t.Error("user agent is not set") - } - }), - } - var nop struct{} - fc.setup().Get("/", &nop) -} diff --git a/pkg/client/deployments.go b/pkg/client/deployments.go deleted file mode 100644 index b28c2d9eb..000000000 --- a/pkg/client/deployments.go +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "os" - fancypath "path" - "path/filepath" - - "github.com/kubernetes/helm/pkg/common" -) - -// ListDeployments lists the deployments in DM. -func (c *Client) ListDeployments() ([]string, error) { - var l []string - _, err := c.Get("deployments", &l) - return l, err -} - -// PostChart sends a chart to DM for deploying. -// -// This returns the location for the new chart, typically of the form -// `helm:repo/bucket/name-version.tgz`. -func (c *Client) PostChart(filename, deployname string) (string, error) { - f, err := os.Open(filename) - if err != nil { - return "", err - } - - u, err := c.url("/v2/charts") - request, err := http.NewRequest("POST", u, f) - if err != nil { - f.Close() - return "", err - } - - // There is an argument to be made for using the legacy x-octet-stream for - // this. But since we control both sides, we should use the standard one. - // Also, gzip (x-compress) is usually treated as a content encoding. In this - // case it probably is not, but it makes more sense to follow the standard, - // even though we don't assume the remote server will strip it off. - request.Header.Add("Content-Type", "application/x-tar") - request.Header.Add("Content-Encoding", "gzip") - request.Header.Add("X-Deployment-Name", deployname) - request.Header.Add("X-Chart-Name", filepath.Base(filename)) - request.Header.Set("User-Agent", c.agent()) - - client := &http.Client{ - Timeout: c.HTTPTimeout, - Transport: c.transport(), - } - - response, err := client.Do(request) - if err != nil { - return "", err - } - - // We only want 201 CREATED. Admittedly, we could accept 200 and 202. - if response.StatusCode != http.StatusCreated { - body, err := ioutil.ReadAll(response.Body) - response.Body.Close() - if err != nil { - return "", err - } - return "", &HTTPError{StatusCode: response.StatusCode, Message: string(body), URL: request.URL} - } - - loc := response.Header.Get("Location") - return loc, nil -} - -// GetDeployment retrieves the supplied deployment -func (c *Client) GetDeployment(name string) (*common.Deployment, error) { - var deployment *common.Deployment - _, err := c.Get(fancypath.Join("deployments", name), &deployment) - return deployment, err -} - -// DeleteDeployment deletes the supplied deployment -func (c *Client) DeleteDeployment(name string) (*common.Deployment, error) { - var deployment *common.Deployment - _, err := c.Delete(filepath.Join("deployments", name), &deployment) - return deployment, err -} - -// DescribeDeployment describes the kubernetes resources of the supplied deployment by fetching the -// latest manifest. -func (c *Client) DescribeDeployment(name string) (*common.Manifest, error) { - var manifest *common.Manifest - deployment, err := c.GetDeployment(name) - if err != nil { - return nil, err - } - if deployment.LatestManifest == "" { - return nil, errors.New("Deployment: '" + name + "' has no manifest") - } - - _, err = c.Get(fancypath.Join("deployments", name, "manifests", deployment.LatestManifest), &manifest) - return manifest, err -} - -// PostDeployment posts a deployment object to the manager service. -func (c *Client) PostDeployment(res *common.Resource) error { - // This is a stop-gap until we get this API cleaned up. - t := common.DeploymentRequest{ - Configuration: common.Configuration{Resources: []*common.Resource{res}}, - Name: res.Name, - } - - data, err := json.Marshal(t) - if err != nil { - return err - } - - var out struct{} - _, err = c.Post("deployments", data, &out) - return err -} diff --git a/pkg/client/deployments_test.go b/pkg/client/deployments_test.go deleted file mode 100644 index 83b8ef4d3..000000000 --- a/pkg/client/deployments_test.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "fmt" - "net/http" - "strings" - "testing" - - "github.com/kubernetes/helm/pkg/common" -) - -func TestListDeployments(t *testing.T) { - fc := &fakeClient{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`["guestbook.yaml"]`)) - }), - } - defer fc.teardown() - - l, err := fc.setup().ListDeployments() - if err != nil { - t.Fatal(err) - } - - if len(l) != 1 { - t.Fatal("expected a single deployment") - } -} - -func TestGetDeployment(t *testing.T) { - fc := &fakeClient{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"name":"guestbook.yaml","id":0,"createdAt":"2016-02-08T12:17:49.251658308-08:00","deployedAt":"2016-02-08T12:17:49.251658589-08:00","modifiedAt":"2016-02-08T12:17:51.177518098-08:00","deletedAt":"0001-01-01T00:00:00Z","state":{"status":"Deployed"},"latestManifest":"manifest-1454962670728402229"}`)) - }), - } - defer fc.teardown() - - d, err := fc.setup().GetDeployment("guestbook.yaml") - if err != nil { - t.Fatal(err) - } - - if d.Name != "guestbook.yaml" { - t.Fatalf("expected deployment name 'guestbook.yaml', got '%s'", d.Name) - } - - if d.State.Status != common.DeployedStatus { - t.Fatalf("expected deployment status 'Deployed', got '%s'", d.State.Status) - } -} - -func TestDescribeDeployment(t *testing.T) { - fc := &fakeClient{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.String(), "manifest") { - w.Write([]byte(`{"deployment":"guestbook.yaml","name":"manifest-1454962670728402229","expandedConfig":{"resources":[{"name":"fe-rc","type":"ReplicationController","state":{"status":"Created"}},{"name":"fe","type":"Service","state":{"status":"Created"}}]}}`)) - } else { - w.Write([]byte(`{"name":"guestbook.yaml","id":0,"createdAt":"2016-02-08T12:17:49.251658308-08:00","deployedAt":"2016-02-08T12:17:49.251658589-08:00","modifiedAt":"2016-02-08T12:17:51.177518098-08:00","deletedAt":"0001-01-01T00:00:00Z","state":{"status":"Deployed"},"latestManifest":"manifest-1454962670728402229"}`)) - } - - }), - } - defer fc.teardown() - - m, err := fc.setup().DescribeDeployment("guestbook.yaml") - if err != nil { - t.Fatal(err) - } - - if m.Deployment != "guestbook.yaml" { - t.Fatalf("expected deployment name 'guestbook.yaml', got '%s'", m.Name) - } - if m.Name != "manifest-1454962670728402229" { - t.Fatalf("expected manifest name 'manifest-1454962670728402229', got '%s'", m.Name) - } - if len(m.ExpandedConfig.Resources) != 2 { - t.Fatalf("expected two resources, got %d", len(m.ExpandedConfig.Resources)) - } - var foundFE = false - var foundFERC = false - for _, r := range m.ExpandedConfig.Resources { - if r.Name == "fe" { - foundFE = true - if r.Type != "Service" { - t.Fatalf("Incorrect type, expected 'Service' got '%s'", r.Type) - } - } - if r.Name == "fe-rc" { - foundFERC = true - if r.Type != "ReplicationController" { - t.Fatalf("Incorrect type, expected 'ReplicationController' got '%s'", r.Type) - } - } - if r.State.Status != common.Created { - t.Fatalf("Incorrect status, expected '%s' got '%s'", common.Created, r.State.Status) - } - } - if !foundFE { - t.Fatalf("didn't find 'fe' in resources") - } - if !foundFERC { - t.Fatalf("didn't find 'fe-rc' in resources") - } -} - -func TestDescribeDeploymentFailedDeployment(t *testing.T) { - var expectedError = "Deployment: 'guestbook.yaml' has no manifest" - fc := &fakeClient{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"name":"guestbook.yaml","createdAt":"2016-04-02T10:41:06.509049871-07:00","deployedAt":"0001-01-01T00:00:00Z","modifiedAt":"2016-04-02T10:41:06.509203582-07:00","deletedAt":"0001-01-01T00:00:00Z","state":{"status":"Failed","errors":["cannot expand configuration:No repository for url gs://kubernetes-charts-testing/redis-2.tgz\n\u0026{[0xc82014efc0]}\n"]},"latestManifest":""}`)) - }), - } - defer fc.teardown() - - m, err := fc.setup().DescribeDeployment("guestbook.yaml") - if err == nil { - t.Fatal("Did not get an error for missing manifest") - } - if err.Error() != expectedError { - t.Fatalf("Unexpected error message, wanted:\n%s\ngot:\n%s", expectedError, err.Error()) - } - if m != nil { - t.Fatal("Got back manifest but shouldn't have") - } -} - -func TestPostDeployment(t *testing.T) { - chartInvocation := &common.Resource{ - Name: "foo", - Type: "helm:example.com/foo/bar", - Properties: map[string]interface{}{ - "port": ":8080", - }, - } - - fc := &fakeClient{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusCreated) - fmt.Fprintln(w, "{}") - }), - } - defer fc.teardown() - - if err := fc.setup().PostDeployment(chartInvocation); err != nil { - t.Fatalf("failed to post deployment: %s", err) - } -} diff --git a/pkg/client/install.go b/pkg/client/install.go deleted file mode 100644 index 22f333587..000000000 --- a/pkg/client/install.go +++ /dev/null @@ -1,237 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "bytes" - "text/template" - - "github.com/Masterminds/sprig" - "github.com/kubernetes/helm/pkg/kubectl" -) - -// Installer is capable of installing DM into Kubernetes. -// -// See InstallYAML. -type Installer struct { - // TODO: At some point we could transform these from maps to structs. - - // Expandybird params are used to render the expandybird manifest. - Expandybird map[string]interface{} - // Resourcifier params are used to render the resourcifier manifest. - Resourcifier map[string]interface{} - // Manager params are used to render the manager manifest. - Manager map[string]interface{} -} - -// NewInstaller creates a new Installer. -func NewInstaller() *Installer { - return &Installer{ - Expandybird: map[string]interface{}{}, - Resourcifier: map[string]interface{}{}, - Manager: map[string]interface{}{}, - } -} - -// Install uses kubectl to install the base DM. -// -// Returns the string output received from the operation, and an error if the -// command failed. -func (i *Installer) Install(runner kubectl.Runner) (string, error) { - b, err := i.expand() - if err != nil { - return "", err - } - - o, err := runner.Create(b) - return string(o), err -} - -func (i *Installer) expand() ([]byte, error) { - var b bytes.Buffer - t := template.Must(template.New("manifest").Funcs(sprig.TxtFuncMap()).Parse(InstallYAML)) - err := t.Execute(&b, i) - return b.Bytes(), err -} - -// IsInstalled checks whether DM has been installed. -func IsInstalled(runner kubectl.Runner) bool { - // Basically, we test "all-or-nothing" here: if this returns without error - // we know that we have both the namespace and the manager API server. - _, err := runner.GetByKind("rc", "manager-rc", "helm") - if err != nil { - return false - } - return true -} - -// InstallYAML is the installation YAML for DM. -const InstallYAML = ` -###################################################################### -# Copyright 2015 The Kubernetes Authors All rights reserved. -# 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. -###################################################################### - ---- -apiVersion: v1 -kind: Namespace -metadata: - labels: - app: helm - name: helm-namespace - name: helm ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: helm - name: expandybird-service - name: expandybird-service - namespace: helm -spec: - ports: - - name: expandybird - port: 8081 - targetPort: 8080 - selector: - app: helm - name: expandybird ---- -apiVersion: v1 -kind: ReplicationController -metadata: - labels: - app: helm - name: expandybird-rc - name: expandybird-rc - namespace: helm -spec: - replicas: 2 - selector: - app: helm - name: expandybird - template: - metadata: - labels: - app: helm - name: expandybird - spec: - containers: - - env: [] - image: {{default "gcr.io/kubernetes-helm/expandybird:latest" .Expandybird.Image}} - name: expandybird - ports: - - containerPort: 8080 - name: expandybird ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: helm - name: resourcifier-service - name: resourcifier-service - namespace: helm -spec: - ports: - - name: resourcifier - port: 8082 - targetPort: 8080 - selector: - app: helm - name: resourcifier ---- -apiVersion: v1 -kind: ReplicationController -metadata: - labels: - app: helm - name: resourcifier-rc - name: resourcifier-rc - namespace: helm -spec: - replicas: 2 - selector: - app: helm - name: resourcifier - template: - metadata: - labels: - app: helm - name: resourcifier - spec: - containers: - - env: [] - image: {{ default "gcr.io/kubernetes-helm/resourcifier:latest" .Resourcifier.Image }} - name: resourcifier - ports: - - containerPort: 8080 - name: resourcifier ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: helm - name: manager-service - name: manager-service - namespace: helm -spec: - ports: - - name: manager - port: 8080 - targetPort: 8080 - selector: - app: helm - name: manager ---- -apiVersion: v1 -kind: ReplicationController -metadata: - labels: - app: helm - name: manager-rc - name: manager-rc - namespace: helm -spec: - replicas: 1 - selector: - app: helm - name: manager - template: - metadata: - labels: - app: helm - name: manager - spec: - containers: - - env: [] - image: {{ default "gcr.io/kubernetes-helm/manager:latest" .Manager.Image }} - name: manager - ports: - - containerPort: 8080 - name: manager -` diff --git a/pkg/client/transport.go b/pkg/client/transport.go deleted file mode 100644 index dc304990a..000000000 --- a/pkg/client/transport.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "fmt" - "io" - "net/http" - "net/http/httputil" - "os" -) - -type debugTransport struct { - // Writer is the logging destination - Writer io.Writer - - http.RoundTripper -} - -// NewDebugTransport returns a debugging implementation of a RoundTripper. -func NewDebugTransport(rt http.RoundTripper) http.RoundTripper { - return debugTransport{ - RoundTripper: rt, - Writer: os.Stderr, - } -} - -func (tr debugTransport) CancelRequest(req *http.Request) { - type canceler interface { - CancelRequest(*http.Request) - } - if cr, ok := tr.transport().(canceler); ok { - cr.CancelRequest(req) - } -} - -func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { - tr.logRequest(req) - resp, err := tr.transport().RoundTrip(req) - if err != nil { - return nil, err - } - tr.logResponse(resp) - return resp, err -} - -func (tr debugTransport) transport() http.RoundTripper { - if tr.RoundTripper != nil { - return tr.RoundTripper - } - return http.DefaultTransport -} - -func (tr debugTransport) logRequest(req *http.Request) { - dump, err := httputil.DumpRequestOut(req, true) - if err != nil { - fmt.Fprintf(tr.Writer, "%s: %s\n", "could not dump request", err) - } - fmt.Fprint(tr.Writer, string(dump)) -} - -func (tr debugTransport) logResponse(resp *http.Response) { - dump, err := httputil.DumpResponse(resp, true) - if err != nil { - fmt.Fprintf(tr.Writer, "%s: %s\n", "could not dump response", err) - } - fmt.Fprint(tr.Writer, string(dump)) -} diff --git a/pkg/client/transport_test.go b/pkg/client/transport_test.go deleted file mode 100644 index bae83c056..000000000 --- a/pkg/client/transport_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "bytes" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestDebugTransport(t *testing.T) { - handler := func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"awesome"}`)) - } - - server := httptest.NewServer(http.HandlerFunc(handler)) - defer server.Close() - - var output bytes.Buffer - - client := &http.Client{ - Transport: debugTransport{ - Writer: &output, - }, - } - - _, err := client.Get(server.URL) - if err != nil { - t.Fatal(err.Error()) - } - - expected := []string{ - "GET / HTTP/1.1", - "Accept-Encoding: gzip", - "HTTP/1.1 200 OK", - "Content-Length: 20", - "Content-Type: application/json", - `{"status":"awesome"}`, - } - actual := output.String() - - for _, match := range expected { - if !strings.Contains(actual, match) { - t.Errorf("Expected %s to contain %s", actual, match) - } - } -} diff --git a/pkg/client/uninstall.go b/pkg/client/uninstall.go deleted file mode 100644 index d6a23702b..000000000 --- a/pkg/client/uninstall.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package client - -import ( - "github.com/kubernetes/helm/pkg/kubectl" -) - -// Uninstall uses kubectl to uninstall the base DM. -// -// Returns the string output received from the operation, and an error if the -// command failed. -func Uninstall(runner kubectl.Runner) (string, error) { - o, err := runner.Delete("helm", "Namespace") - return string(o), err -} diff --git a/pkg/common/types.go b/pkg/common/types.go deleted file mode 100644 index 6d6a124c9..000000000 --- a/pkg/common/types.go +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package common - -import ( - "time" -) - -// Deployment defines a deployment that describes -// the creation, modification and/or deletion of a set of resources. -type Deployment struct { - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt,omitempty"` - DeployedAt time.Time `json:"deployedAt,omitempty"` - ModifiedAt time.Time `json:"modifiedAt,omitempty"` - DeletedAt time.Time `json:"deletedAt,omitempty"` - State *DeploymentState `json:"state,omitempty"` - LatestManifest string `json:"latestManifest,omitEmpty"` -} - -// NewDeployment creates a new deployment. -func NewDeployment(name string) *Deployment { - return &Deployment{ - Name: name, - CreatedAt: time.Now(), - State: &DeploymentState{Status: CreatedStatus}, - } -} - -// DeploymentState describes the state of a resource. It is set to the terminal -// state depending on the outcome of an operation on the deployment. -type DeploymentState struct { - Status DeploymentStatus `json:"status,omitempty"` - Errors []string `json:"errors,omitempty"` -} - -// DeploymentStatus is an enumeration type for the status of a deployment. -type DeploymentStatus string - -// These constants implement the DeploymentStatus enumeration type. -const ( - CreatedStatus DeploymentStatus = "Created" - DeletedStatus DeploymentStatus = "Deleted" - DeployedStatus DeploymentStatus = "Deployed" - FailedStatus DeploymentStatus = "Failed" - ModifiedStatus DeploymentStatus = "Modified" -) - -func (s DeploymentStatus) String() string { - return string(s) -} - -// Manifest contains the input configuration for a deployment, the fully -// expanded configuration, and the layout structure of the manifest. -// -type Manifest struct { - Deployment string `json:"deployment,omitempty"` - Name string `json:"name,omitempty"` - InputConfig *Configuration `json:"inputConfig,omitempty"` - ExpandedConfig *Configuration `json:"expandedConfig,omitempty"` - Layout *Layout `json:"layout,omitempty"` -} - -// DeploymentRequest defines the manager API to create deployments. -type DeploymentRequest struct { - Configuration - Name string `json:"name"` -} - -// ChartInstance defines the metadata for an instantiation of a chart. -type ChartInstance struct { - Name string `json:"name"` // instance name - Type string `json:"type"` // instance type - Deployment string `json:"deployment"` // deployment name - Manifest string `json:"manifest"` // manifest name - Path string `json:"path"` // JSON path within manifest -} - -// LayoutResource defines the structure of resources in the manifest layout. -type LayoutResource struct { - Resource - Layout -} - -// Layout defines the structure of a layout as returned from expansion. -type Layout struct { - Resources []*LayoutResource `json:"resources,omitempty"` -} - -// Configuration describes a set of resources in a form -// that can be instantiated. -type Configuration struct { - Resources []*Resource `json:"resources"` -} - -// ResourceStatus is an enumeration type for the status of a resource. -type ResourceStatus string - -// These constants implement the resourceStatus enumeration type. -const ( - Created ResourceStatus = "Created" - Failed ResourceStatus = "Failed" - Aborted ResourceStatus = "Aborted" -) - -// ResourceState describes the state of a resource. -// Status is set during resource creation and is a terminal state. -type ResourceState struct { - Status ResourceStatus `json:"status,omitempty"` - SelfLink string `json:"selflink,omitempty"` - Errors []string `json:"errors,omitempty"` -} - -// Resource describes a resource in a configuration. A resource has -// a name, a type and a set of properties. The name and type are used -// to identify the resource in Kubernetes. The properties are passed -// to Kubernetes as the resource configuration. -type Resource struct { - Name string `json:"name"` - Type string `json:"type"` - Properties map[string]interface{} `json:"properties,omitempty"` - State *ResourceState `json:"state,omitempty"` -} - -// TODO: Remove the following section when the refactoring of templates is complete. - -// Template describes a set of resources to be deployed. -// Manager expands a Template into a Configuration, which -// describes the set in a form that can be instantiated. -type Template struct { - Name string `json:"name"` - Content string `json:"content"` - Imports []*ImportFile `json:"imports"` -} - -// ImportFile describes a base64 encoded file imported by a Template. -type ImportFile struct { - Name string `json:"name,omitempty"` - Path string `json:"path,omitempty"` // Actual URL for the file - Content string `json:"content"` -} diff --git a/pkg/doc.go b/pkg/doc.go deleted file mode 100644 index 17fad34d3..000000000 --- a/pkg/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pkg contains all libraries for Helm. -package pkg diff --git a/pkg/expansion/service.go b/pkg/expansion/service.go deleted file mode 100644 index 8a589a063..000000000 --- a/pkg/expansion/service.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expansion - -import ( - "github.com/kubernetes/helm/pkg/util" - - "errors" - "fmt" - "net/http" - - restful "github.com/emicklei/go-restful" -) - -// A Service wraps a web service that performs template expansion. -type Service struct { - webService *restful.WebService - server *http.Server - container *restful.Container -} - -// NewService encapsulates code to open an HTTP server on the given address:port that serves the -// expansion API using the given Expander backend to do the actual expansion. After calling -// NewService, call ListenAndServe to start the returned service. -func NewService(address string, port int, backend Expander) *Service { - - restful.EnableTracing(true) - webService := new(restful.WebService) - webService.Consumes(restful.MIME_JSON) - webService.Produces(restful.MIME_JSON) - handler := func(req *restful.Request, resp *restful.Response) { - util.LogHandlerEntry("expansion service", req.Request) - request := &ServiceRequest{} - if err := req.ReadEntity(&request); err != nil { - badRequest(resp, err.Error()) - return - } - - reqMsg := fmt.Sprintf("\nhandling request:\n%s\n", util.ToYAMLOrError(request)) - util.LogHandlerText("expansion service", reqMsg) - response, err := backend.ExpandChart(request) - if err != nil { - badRequest(resp, fmt.Sprintf("error expanding chart: %s", err)) - return - } - - util.LogHandlerExit("expansion service", http.StatusOK, "OK", resp.ResponseWriter) - respMsg := fmt.Sprintf("\nreturning response:\n%s\n", util.ToYAMLOrError(response.Resources)) - util.LogHandlerText("expansion service", respMsg) - resp.WriteEntity(response) - } - webService.Route( - webService.POST("/expand"). - To(handler). - Doc("Expand a chart."). - Reads(&ServiceRequest{}). - Writes(&ServiceResponse{})) - - container := restful.DefaultContainer - container.Add(webService) - server := &http.Server{ - Addr: fmt.Sprintf("%s:%d", address, port), - Handler: container, - } - - return &Service{ - webService: webService, - server: server, - container: container, - } -} - -// ListenAndServe blocks forever, handling expansion requests. -func (s *Service) ListenAndServe() error { - return s.server.ListenAndServe() -} - -func badRequest(resp *restful.Response, message string) { - statusCode := http.StatusBadRequest - util.LogHandlerExit("expansion service", statusCode, message, resp.ResponseWriter) - resp.WriteError(statusCode, errors.New(message)) -} diff --git a/pkg/expansion/service_test.go b/pkg/expansion/service_test.go deleted file mode 100644 index 9bce098fb..000000000 --- a/pkg/expansion/service_test.go +++ /dev/null @@ -1,275 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expansion - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "reflect" - "testing" - - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/util" -) - -var ( - testRequest = &ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "test_invocation", - Type: "Test Chart", - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Name: "TestChart", - Expander: &chart.Expander{ - Name: "FakeExpander", - Entrypoint: "None", - }, - }, - Members: []*chart.Member{ - { - Path: "templates/testfile", - Content: []byte("test"), - }, - }, - }, - } - testResponse = &ServiceResponse{ - Resources: []interface{}{"test"}, - } -) - -// A FakeExpander returns testResponse if it was given testRequest, otherwise raises an error. -type FakeExpander struct { -} - -func (fake *FakeExpander) ExpandChart(req *ServiceRequest) (*ServiceResponse, error) { - if reflect.DeepEqual(req, testRequest) { - return testResponse, nil - } - return nil, fmt.Errorf("Test Error Response") -} - -func wrapReader(value interface{}) (io.Reader, error) { - valueJSON, err := json.Marshal(value) - if err != nil { - return nil, err - } - return bytes.NewReader(valueJSON), nil -} - -func GeneralTest(t *testing.T, httpMeth string, url string, contentType string, req *ServiceRequest, - expResponse *ServiceResponse, expStatus int) { - service := NewService("127.0.0.1", 8080, &FakeExpander{}) - handlerTester := util.NewHandlerTester(service.container) - reader, err := wrapReader(testRequest) - if err != nil { - t.Fatalf("unexpected error: %s\n", err) - } - w, err := handlerTester(httpMeth, url, contentType, reader) - if err != nil { - t.Fatalf("unexpected error: %s\n", err) - } - var data = w.Body.Bytes() - if w.Code != expStatus { - t.Fatalf("wrong status code:\nwant: %d\ngot: %d\ncontent: %s\n", expStatus, w.Code, data) - } - if expResponse != nil { - var response ServiceResponse - err = json.Unmarshal(data, &response) - if err != nil { - t.Fatalf("Response could not be unmarshalled: %s\nresponse: %s", err, string(data)) - } - if !reflect.DeepEqual(response, *expResponse) { - t.Fatalf("Response did not match.\nwant: %s\ngot: %s\n", expResponse, response) - } - } -} - -func TestInvalidMethod(t *testing.T) { - GeneralTest(t, "GET", "/expand", "application/json", nil, nil, http.StatusMethodNotAllowed) -} - -func TestInvalidURL(t *testing.T) { - GeneralTest(t, "POST", "/erroneus", "application/json", testRequest, nil, http.StatusNotFound) -} - -func TestInvalidMimeType(t *testing.T) { - GeneralTest(t, "POST", "/expand", "erroneus", nil, nil, http.StatusUnsupportedMediaType) -} - -func TestExpandOK(t *testing.T) { - GeneralTest(t, "POST", "/expand", "application/json", testRequest, testResponse, http.StatusOK) -} - -/* -type ServiceWrapperTestCase struct { - Description string - HTTPMethod string - ServiceURLPath string - ContentType string - StatusCode int -} - -var ServiceWrapperTestCases = []ServiceWrapperTestCase{ - { - "expect error for invalid HTTP verb", - httpGETMethod, - validServiceURL, - jsonContentType, - http.StatusMethodNotAllowed, - }, - { - "expect error for invalid URL path", - httpPOSTMethod, - invalidServiceURL, - jsonContentType, - http.StatusNotFound, - }, - { - "expect error for invalid content type", - httpPOSTMethod, - validServiceURL, - invalidContentType, - http.StatusUnsupportedMediaType, - }, - { - "expect success", - httpPOSTMethod, - validServiceURL, - jsonContentType, - http.StatusOK, - }, -} - -func TestServiceWrapper(t *testing.T) { - backend := expander.NewExpander("../../../expansion/expansion.py") - wrapper := NewService(NewExpansionHandler(backend)) - container := restful.NewContainer() - container.ServeMux = http.NewServeMux() - wrapper.Register(container) - handlerTester := util.NewHandlerTester(container) - for _, swtc := range ServiceWrapperTestCases { - reader := GetTemplateReader(t, swtc.Description, inputFileName) - w, err := handlerTester(swtc.HTTPMethod, swtc.ServiceURLPath, swtc.ContentType, reader) - if err != nil { - t.Errorf("error in test case '%s': %s\n", swtc.Description, err) - } - - if w.Code != http.StatusOK { - if w.Code != swtc.StatusCode { - message := fmt.Sprintf("test returned code:%d, status: %s", w.Code, w.Body.String()) - t.Errorf("error in test case '%s': %s\n", swtc.Description, message) - } - } else { - if swtc.StatusCode != http.StatusOK { - t.Errorf("expected error did not occur in test case '%s': want: %d have: %d\n", - swtc.Description, swtc.StatusCode, w.Code) - } - - body := w.Body.Bytes() - actualResponse := &expander.ExpansionResponse{} - if err := json.Unmarshal(body, actualResponse); err != nil { - t.Errorf("error in test case '%s': %s\n", swtc.Description, err) - } - - actualResult, err := actualResponse.Unmarshal() - if err != nil { - t.Errorf("error in test case '%s': %s\n", swtc.Description, err) - } - - expectedOutput := GetOutputString(t, swtc.Description) - expectedResult := expandOutputOrDie(t, expectedOutput, swtc.Description) - - if !reflect.DeepEqual(expectedResult, actualResult) { - message := fmt.Sprintf("want: %s\nhave: %s\n", - util.ToYAMLOrError(expectedResult), util.ToYAMLOrError(actualResult)) - t.Errorf("error in test case '%s':\n%s\n", swtc.Description, message) - } - } - } -} - -type ExpansionHandlerTestCase struct { - Description string - TemplateFileName string -} - -var ExpansionHandlerTestCases = []ExpansionHandlerTestCase{ - { - "expect error while expanding template", - "../test/InvalidFileName.yaml", - }, - { - "expect error while marshaling output", - "../test/InvalidTypeName.yaml", - }, -} - -var malformedExpansionOutput = []byte(` -this: is: invalid: yaml: -`) - -type mockExpander struct { -} - -// ExpandTemplate passes the given configuration to the expander and returns the -// expanded configuration as a string on success. -func (e *mockExpander) ExpandTemplate(template *common.Template) (string, error) { - switch template.Name { - case "InvalidFileName.yaml": - return "", fmt.Errorf("expansion error") - case "InvalidTypeName.yaml": - return string(malformedExpansionOutput), nil - } - - panic("unknown test case") -} - -func TestExpansionHandler(t *testing.T) { - backend := &mockExpander{} - wrapper := NewService(NewExpansionHandler(backend)) - container := restful.DefaultContainer - wrapper.Register(container) - handlerTester := util.NewHandlerTester(container) - for _, ehtc := range ExpansionHandlerTestCases { - reader := GetTemplateReader(t, ehtc.Description, ehtc.TemplateFileName) - w, err := handlerTester(httpPOSTMethod, validServiceURL, jsonContentType, reader) - if err != nil { - t.Errorf("error in test case '%s': %s\n", ehtc.Description, err) - } - - if w.Code != http.StatusBadRequest { - t.Errorf("expected error did not occur in test case '%s': want: %d have: %d\n", - ehtc.Description, http.StatusBadRequest, w.Code) - } - } -} - -func expandOutputOrDie(t *testing.T, output, description string) *expander.ExpansionResult { - result, err := expander.NewExpansionResult(output) - if err != nil { - t.Errorf("cannot expand output for test case '%s': %s\n", description, err) - } - - return result -} -*/ diff --git a/pkg/expansion/types.go b/pkg/expansion/types.go deleted file mode 100644 index bd9e0723b..000000000 --- a/pkg/expansion/types.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expansion - -import ( - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" -) - -// ServiceRequest defines the API to expander. -type ServiceRequest struct { - ChartInvocation *common.Resource `json:"chart_invocation"` - Chart *chart.Content `json:"chart"` -} - -// ServiceResponse defines the API to expander. -type ServiceResponse struct { - Resources []interface{} `json:"resources"` -} - -// Expander abstracts interactions with the expander and deployer services. -type Expander interface { - ExpandChart(request *ServiceRequest) (*ServiceResponse, error) -} diff --git a/pkg/expansion/validate.go b/pkg/expansion/validate.go deleted file mode 100644 index b172c73ee..000000000 --- a/pkg/expansion/validate.go +++ /dev/null @@ -1,126 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expansion - -import ( - "bytes" - "fmt" - - "github.com/ghodss/yaml" - "github.com/juju/gojsonschema" - "github.com/kubernetes/helm/pkg/chart" -) - -// ValidateRequest does basic sanity checks on the request. -func ValidateRequest(request *ServiceRequest) error { - if request.ChartInvocation == nil { - return fmt.Errorf("Request does not have invocation field") - } - if request.Chart == nil { - return fmt.Errorf("Request does not have chart field") - } - - chartInv := request.ChartInvocation - chartFile := request.Chart.Chartfile - - l, err := chart.Parse(chartInv.Type) - if err != nil { - return fmt.Errorf("cannot parse chart reference %s: %s", chartInv.Type, err) - } - - if l.Name != chartFile.Name { - return fmt.Errorf("Chart invocation type (%s) does not match provided chart (%s)", chartInv.Type, chartFile.Name) - } - - if chartFile.Expander == nil { - message := fmt.Sprintf("Chart JSON does not have expander field") - return fmt.Errorf("%s: %s", chartInv.Name, message) - } - - return nil -} - -// ValidateProperties validates the properties in the chart invocation against the schema file in -// the chart itself, which is assumed to be JSONschema. It also modifies a copy of the request to -// add defaults values if properties are not provided (according to the default field in -// JSONschema), and returns this copy. -func ValidateProperties(request *ServiceRequest) (*ServiceRequest, error) { - - schemaFilename := request.Chart.Chartfile.Schema - - if schemaFilename == "" { - // No schema, so perform no validation. - return request, nil - } - - chartInv := request.ChartInvocation - - var schemaBytes *[]byte - for _, f := range request.Chart.Members { - if f.Path == schemaFilename { - schemaBytes = &f.Content - } - } - if schemaBytes == nil { - return nil, fmt.Errorf("%s: The schema referenced from the Chart.yaml cannot be found: %s", - chartInv.Name, schemaFilename) - } - var schemaDoc interface{} - - if err := yaml.Unmarshal(*schemaBytes, &schemaDoc); err != nil { - return nil, fmt.Errorf("%s: %s was not valid YAML: %v", - chartInv.Name, schemaFilename, err) - } - - // Build a schema object - schema, err := gojsonschema.NewSchema(gojsonschema.NewGoLoader(schemaDoc)) - if err != nil { - return nil, err - } - - // Do validation - result, err := schema.Validate(gojsonschema.NewGoLoader(request.ChartInvocation.Properties)) - if err != nil { - return nil, err - } - - // Need to concat errors here - if !result.Valid() { - var message bytes.Buffer - message.WriteString("Properties failed validation:\n") - for _, err := range result.Errors() { - message.WriteString(fmt.Sprintf("- %s", err)) - } - return nil, fmt.Errorf("%s: %s", chartInv.Name, message.String()) - } - - // Fill in defaults (after validation). - modifiedProperties, err := schema.InsertDefaults(request.ChartInvocation.Properties) - if err != nil { - return nil, err - } - - modifiedResource := *request.ChartInvocation - modifiedResource.Properties = modifiedProperties - - modifiedRequest := &ServiceRequest{ - ChartInvocation: &modifiedResource, - Chart: request.Chart, - } - - return modifiedRequest, nil -} diff --git a/pkg/expansion/validate_test.go b/pkg/expansion/validate_test.go deleted file mode 100644 index f6cd848bf..000000000 --- a/pkg/expansion/validate_test.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package expansion - -import ( - "fmt" - "reflect" - "strings" - "testing" - - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/common" -) - -func testPropertiesValidation(t *testing.T, req *ServiceRequest, expRequest *ServiceRequest, expError string) { - modifiedRequest, err := ValidateProperties(req) - if err != nil { - message := err.Error() - if expRequest != nil || !strings.Contains(message, expError) { - t.Fatalf("unexpected error: %v\n", err) - } - } else { - if expRequest == nil { - t.Fatalf("expected error did not occur: %s\n", expError) - } - if !reflect.DeepEqual(modifiedRequest, expRequest) { - message := fmt.Sprintf("want:\n%s\nhave:\n%s\n", expRequest, modifiedRequest) - t.Fatalf("output mismatch:\n%s\n", message) - } - } -} - -func TestNoSchema(t *testing.T) { - req := &ServiceRequest{ - ChartInvocation: &common.Resource{ - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{}, - Members: []*chart.Member{}, - }, - } - testPropertiesValidation(t, req, req, "") // Returns it unchanged. -} - -func TestSchemaNotFound(t *testing.T) { - testPropertiesValidation( - t, - &ServiceRequest{ - ChartInvocation: &common.Resource{ - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Schema: "Schema.yaml", - }, - }, - }, - nil, // No response to check. - "The schema referenced from the Chart.yaml cannot be found: Schema.yaml", - ) -} - -var schemaContent = []byte(` - required: ["prop2"] - additionalProperties: false - properties: - prop1: - description: Nice description. - type: integer - default: 42 - prop2: - description: Nice description. - type: string -`) - -func TestSchema(t *testing.T) { - req := &ServiceRequest{ - ChartInvocation: &common.Resource{ - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop2": "foo", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Schema: "Schema.yaml", - }, - Members: []*chart.Member{ - { - Path: "Schema.yaml", - Content: schemaContent, - }, - }, - }, - } - // No defaults, returns it unchanged: - testPropertiesValidation(t, req, req, "") -} - -func TestBadProperties(t *testing.T) { - testPropertiesValidation( - t, - &ServiceRequest{ - ChartInvocation: &common.Resource{ - Properties: map[string]interface{}{ - "prop1": 3.0, - "prop3": map[string]interface{}{}, - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Schema: "Schema.yaml", - }, - Members: []*chart.Member{ - { - Path: "Schema.yaml", - Content: schemaContent, - }, - }, - }, - }, - nil, - "Properties failed validation:", - ) -} - -func TestDefault(t *testing.T) { - testPropertiesValidation( - t, - &ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "TestName", - Type: "TestType", - Properties: map[string]interface{}{ - "prop2": "ok", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Schema: "Schema.yaml", - }, - Members: []*chart.Member{ - { - Path: "Schema.yaml", - Content: schemaContent, - }, - }, - }, - }, - &ServiceRequest{ - ChartInvocation: &common.Resource{ - Name: "TestName", - Type: "TestType", - Properties: map[string]interface{}{ - "prop1": 42.0, - "prop2": "ok", - }, - }, - Chart: &chart.Content{ - Chartfile: &chart.Chartfile{ - Schema: "Schema.yaml", - }, - Members: []*chart.Member{ - { - Path: "Schema.yaml", - Content: schemaContent, - }, - }, - }, - }, - "", // Error - ) -} diff --git a/pkg/format/messages.go b/pkg/format/messages.go deleted file mode 100644 index 858b93394..000000000 --- a/pkg/format/messages.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package format - -import ( - "bytes" - "fmt" - "io" - "os" - "reflect" - "sort" - - "github.com/ghodss/yaml" -) - -// Stdout is the output this library will write to. -var Stdout io.Writer = os.Stdout - -// Stderr is the error output this library will write to. -var Stderr io.Writer = os.Stderr - -// This is all just placeholder. - -// Err prints an error message to Stderr. -func Err(message interface{}, v ...interface{}) { - var msg string - val := reflect.Indirect(reflect.ValueOf(message)) - if val.Kind() == reflect.String { - msg = message.(string) - } else if z, ok := message.(fmt.Stringer); ok { - msg = z.String() - } else if z, ok := message.(error); ok { - msg = z.Error() - } - - msg = "[ERROR] " + msg + "\n" - fmt.Fprintf(Stderr, msg, v...) -} - -// Info prints an informational message to Stdout. -func Info(msg string, v ...interface{}) { - msg = "[INFO] " + msg + "\n" - fmt.Fprintf(Stdout, msg, v...) -} - -// Msg prints a raw message to Stdout. -func Msg(msg string, v ...interface{}) { - fmt.Fprintf(Stdout, msg, v...) -} - -// Success is an achievement marked by pretty output. -func Success(msg string, v ...interface{}) { - msg = "[Success] " + msg + "\n" - fmt.Fprintf(Stdout, msg, v...) -} - -// Warning emits a warning message. -func Warning(msg string, v ...interface{}) { - msg = "[Warning] " + msg + "\n" - fmt.Fprintf(Stdout, msg, v...) -} - -// List prints a list of strings to Stdout. -// -// This sorts lexicographically. -func List(list []string) { - sort.Strings(list) - // Buffer and then flush all at once to avoid concurrency-based interleaving. - var b bytes.Buffer - for _, v := range list { - if v == "" { - v = "[empty]" - } - fmt.Fprintf(&b, "%s\n", v) - } - Stdout.Write(b.Bytes()) -} - -// YAML prints an object in YAML format. -func YAML(v interface{}) error { - y, err := yaml.Marshal(v) - if err != nil { - return fmt.Errorf("Failed to serialize to yaml: %s", v.(string)) - } - - Msg(string(y)) - return nil -} diff --git a/pkg/format/messages_test.go b/pkg/format/messages_test.go deleted file mode 100644 index b08085881..000000000 --- a/pkg/format/messages_test.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package format - -import ( - "bytes" - "os" - "testing" -) - -func TestList(t *testing.T) { - var b bytes.Buffer - in := []string{"ddd", "ccc", "aaa", "bbb"} - expect := "aaa\nbbb\nccc\nddd\n" - Stdout = &b - defer func() { Stdout = os.Stdout }() - - List(in) - if b.String() != expect { - t.Errorf("Expected %q, got %q", expect, b.String()) - } -} diff --git a/pkg/httputil/doc.go b/pkg/httputil/doc.go deleted file mode 100644 index 2c33cf5b2..000000000 --- a/pkg/httputil/doc.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -/*Package httputil provides common HTTP tools. - -This package provides tools for working with HTTP requests and responses. -*/ -package httputil diff --git a/pkg/httputil/encoder.go b/pkg/httputil/encoder.go deleted file mode 100644 index c491a4449..000000000 --- a/pkg/httputil/encoder.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package httputil - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "mime" - "net/http" - "reflect" - "strings" - - "github.com/ghodss/yaml" -) - -// DefaultEncoder is an *AcceptEncoder with the default application/json encoding. -var DefaultEncoder = &AcceptEncoder{DefaultEncoding: "application/json", MaxReadLen: DefaultMaxReadLen} - -// DefaultMaxReadLen is the default maximum length to accept in an HTTP request body. -var DefaultMaxReadLen int64 = 1024 * 1024 - -// Encoder takes input and translate it to an expected encoded output. -// -// Implementations of encoders may use details of the HTTP request and response -// to correctly encode an object for return to the client. -// -// Encoders are expected to produce output, even if that output is an error -// message. -type Encoder interface { - // Encode encoders a given response - // - // When an encoder fails, it logs any necessary data and then responds to - // the client. - // - // The integer must be a valid http.Status* status code. - Encode(http.ResponseWriter, *http.Request, interface{}, int) - - // Decode reads and decodes a request body. - Decode(http.ResponseWriter, *http.Request, interface{}) error -} - -// Decode decodes a request body using the DefaultEncoder. -func Decode(w http.ResponseWriter, r *http.Request, v interface{}) error { - return DefaultEncoder.Decode(w, r, v) -} - -// Encode encodes a request body using the DefaultEncoder. -func Encode(w http.ResponseWriter, r *http.Request, v interface{}, statusCode int) { - DefaultEncoder.Encode(w, r, v, statusCode) -} - -// AcceptEncoder uses the accept headers on a request to determine the response type. -// -// It supports the following encodings: -// - application/json: passed to encoding/json.Marshal -// - text/yaml: passed to gopkg.in/yaml.v2.Marshal -// - text/plain: passed to fmt.Sprintf("%V") -// -// It treats `application/x-yaml` as `text/yaml`. -type AcceptEncoder struct { - DefaultEncoding string - MaxReadLen int64 -} - -// Encode encodeds the given interface to the first available type in the Accept header. -func (e *AcceptEncoder) Encode(w http.ResponseWriter, r *http.Request, out interface{}, statusCode int) { - a := r.Header.Get("accept") - fn := encoders[e.DefaultEncoding] - mt := e.DefaultEncoding - if a != "" { - mt, fn = e.parseAccept(a) - } - - data, err := fn(out) - if err != nil { - Fatal(w, r, "Could not marshal data: %s", err) - return - } - w.Header().Add("content-type", mt) - w.WriteHeader(statusCode) - w.Write(data) -} - -// Decode decodes the given request into the given interface. -// -// It selects the marshal based on the value of the Content-Type header. If no -// viable decoder is found, it attempts to use the DefaultEncoder. -func (e *AcceptEncoder) Decode(w http.ResponseWriter, r *http.Request, v interface{}) error { - if e.MaxReadLen > 0 && r.ContentLength > int64(e.MaxReadLen) { - RequestEntityTooLarge(w, r, fmt.Sprintf("Max len is %d, submitted len is %d.", e.MaxReadLen, r.ContentLength)) - } - data, err := ioutil.ReadAll(r.Body) - r.Body.Close() - if err != nil { - return err - } - - ct := r.Header.Get("content-type") - mt, _, err := mime.ParseMediaType(ct) - if err != nil { - mt = "application/x-octet-stream" - } - - for n, fn := range decoders { - if n == mt { - return fn(data, v) - } - } - - return decoders[e.DefaultEncoding](data, v) -} - -// parseAccept parses the value of an Accept: header and returns the best match. -// -// This returns the matched MIME type and the Marshal function. -func (e *AcceptEncoder) parseAccept(h string) (string, Marshaler) { - - keys := strings.Split(h, ",") - for _, k := range keys { - mt, _, err := mime.ParseMediaType(k) - if err != nil { - continue - } - if enc, ok := encoders[mt]; ok { - return mt, enc - } - } - return e.DefaultEncoding, encoders[e.DefaultEncoding] -} - -// Marshaler marshals an interface{} into a []byte. -type Marshaler func(interface{}) ([]byte, error) - -// Unmarshaler unmarshals []byte to an interface{}. -type Unmarshaler func([]byte, interface{}) error - -var encoders = map[string]Marshaler{ - "application/json": json.Marshal, - "text/yaml": yaml.Marshal, - "application/x-yaml": yaml.Marshal, - "text/plain": textMarshal, -} - -var decoders = map[string]Unmarshaler{ - "application/json": json.Unmarshal, - "text/yaml": yaml.Unmarshal, - "application/x-yaml": yaml.Unmarshal, -} - -// ErrUnsupportedKind indicates that the marshal cannot marshal a particular Go Kind (e.g. struct or chan). -var ErrUnsupportedKind = errors.New("unsupported kind") - -// textMarshal marshals v into a text representation ONLY IN NARROW CASES. -// -// An error will have its Error() method called. -// A fmt.Stringer will have its String() method called. -// Scalar types will be marshaled with fmt.Sprintf("%v"). -// -// This will only marshal scalar types for securoty reasons (namely, we don't -// want the possibility of forcing exposure of non-exported data or ptr -// addresses, etc.) -func textMarshal(v interface{}) ([]byte, error) { - switch s := v.(type) { - case error: - return []byte(s.Error()), nil - case fmt.Stringer: - return []byte(s.String()), nil - } - - // Error on kinds we don't support. - val := reflect.Indirect(reflect.ValueOf(v)) - switch val.Kind() { - case reflect.Invalid, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, - reflect.Map, reflect.Ptr, reflect.Slice, reflect.Struct, reflect.UnsafePointer: - return []byte{}, ErrUnsupportedKind - } - return []byte(fmt.Sprintf("%v", v)), nil -} diff --git a/pkg/httputil/encoder_test.go b/pkg/httputil/encoder_test.go deleted file mode 100644 index ba5274aa7..000000000 --- a/pkg/httputil/encoder_test.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package httputil - -import ( - "bytes" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" -) - -var _ Encoder = &AcceptEncoder{} - -func TestParseAccept(t *testing.T) { - e := &AcceptEncoder{ - DefaultEncoding: "application/json", - } - tests := map[string]string{ - "": e.DefaultEncoding, - "*/*": e.DefaultEncoding, - // To stay true to spec, this _should_ be an error. But our thought - // on this case is that we'd rather send a default format. - "audio/*; q=0.2, audio/basic": e.DefaultEncoding, - "text/html; q=0.8, text/yaml,application/json": "text/yaml", - "application/x-yaml; foo=bar": "application/x-yaml", - "text/monkey, TEXT/YAML ; zoom=zoom ": "text/yaml", - } - - for in, expects := range tests { - mt, enc := e.parseAccept(in) - if mt != expects { - t.Errorf("Expected %q, got %q", expects, mt) - continue - } - _, err := enc([]string{"hello", "world"}) - if err != nil { - t.Fatalf("Failed to marshal: %s", err) - } - } -} - -func TestTextMarshal(t *testing.T) { - tests := map[string]interface{}{ - "foo": "foo", - "5": 5, - "stinky cheese": errors.New("stinky cheese"), - } - for expect, in := range tests { - if o, err := textMarshal(in); err != nil || string(o) != expect { - t.Errorf("Expected %q, got %q", expect, o) - } - } - - if _, err := textMarshal(struct{ foo int }{5}); err != ErrUnsupportedKind { - t.Fatalf("Expected unsupported kind, got %v", err) - } -} - -type encDec struct { - Name string -} - -func TestDefaultEncoder(t *testing.T) { - in := &encDec{Name: "Foo"} - var out, out2 encDec - - fn := func(w http.ResponseWriter, r *http.Request) { - if err := Decode(w, r, &out); err != nil { - t.Fatalf("Failed to decode data: %s", err) - } - if out.Name != in.Name { - t.Fatalf("Expected %q, got %q", in.Name, out.Name) - } - Encode(w, r, out, http.StatusOK) - } - s := httptest.NewServer(http.HandlerFunc(fn)) - defer s.Close() - - data, err := json.Marshal(in) - if err != nil { - t.Fatalf("Failed to marshal JSON: %s", err) - } - req, err := http.NewRequest("GET", s.URL, bytes.NewBuffer(data)) - if err != nil { - t.Fatal(err) - } - req.Header.Set("content-type", "application/json") - - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Errorf("Failed request: %s", err) - } - if res.StatusCode != http.StatusOK { - t.Errorf("Expected 200, got %d", res.StatusCode) - } - - data, err = ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(data, &out2); err != nil { - t.Fatal(err) - } - if out2.Name != in.Name { - t.Errorf("Expected final output to have name %q, got %q", in.Name, out2.Name) - } -} - -func TestAcceptEncoderEncoder(t *testing.T) { - enc := &AcceptEncoder{ - DefaultEncoding: "application/json", - } - fn := func(w http.ResponseWriter, r *http.Request) { - enc.Encode(w, r, []string{"hello", "world"}, http.StatusOK) - } - s := httptest.NewServer(http.HandlerFunc(fn)) - defer s.Close() - - res, err := http.Get(s.URL) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Fatalf("Unexpected response code %d", res.StatusCode) - } - if mt := res.Header.Get("content-type"); mt != "application/json" { - t.Errorf("Unexpected content type: %q", mt) - } - - data, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatalf("Failed to read response body: %s", err) - } - - out := []string{} - if err := json.Unmarshal(data, &out); err != nil { - t.Fatalf("Failed to unmarshal JSON: %s", err) - } - - if out[0] != "hello" { - t.Fatalf("Unexpected JSON data in slot 0: %s", out[0]) - } -} diff --git a/pkg/httputil/httperrors.go b/pkg/httputil/httperrors.go deleted file mode 100644 index 6be920a73..000000000 --- a/pkg/httputil/httperrors.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package httputil - -import ( - "fmt" - "log" - "net/http" -) - -const ( - // LogAccess is for logging access messages. Form: Access r.Method, r.URL - LogAccess = "Access: %s %s" - // LogNotFound is for logging 404 errors. Form: Not Found r.Method, r.URL - LogNotFound = "Not Found: %s %s" - // LogFatal is for logging 500 errors. Form: Internal Server Error r.Method r.URL message - LogFatal = "Internal Server Error: %s %s %s" - // LogBadRequest logs 400 errors. - LogBadRequest = "Bad Request: %s %s %s" -) - -// Error represents an HTTP error that can be converted to structured types. -// -// For example, and error can be serialized to JSON or YAML. Likewise, the -// string marshal can convert it to a string. -type Error struct { - Status string `json:"status"` - Msg string `json:"message, omitempty"` -} - -// Error implements the error interface. -func (e *Error) Error() string { - return fmt.Sprintf("%s: %s", e.Status, e.Msg) -} - -// NotFound writes a 404 error to the client and logs an error. -func NotFound(w http.ResponseWriter, r *http.Request) { - msg := fmt.Sprintf(LogNotFound, r.Method, r.URL) - log.Println(msg) - writeErr(w, r, msg, http.StatusNotFound) -} - -// RequestEntityTooLarge writes a 413 to the client and logs an error. -func RequestEntityTooLarge(w http.ResponseWriter, r *http.Request, msg string) { - log.Println(msg) - writeErr(w, r, msg, http.StatusRequestEntityTooLarge) -} - -// BadRequest writes an HTTP 400. -func BadRequest(w http.ResponseWriter, r *http.Request, err error) { - log.Printf(LogBadRequest, r.Method, r.URL, err) - writeErr(w, r, err.Error(), http.StatusBadRequest) -} - -// writeErr formats and writes the error using the default encoder. -func writeErr(w http.ResponseWriter, r *http.Request, msg string, status int) { - DefaultEncoder.Encode(w, r, &Error{Status: http.StatusText(status), Msg: msg}, status) -} - -// Fatal writes a 500 response to the client and logs the message. -// -// Additional arguments are past into the the formatter as params to msg. -func Fatal(w http.ResponseWriter, r *http.Request, msg string, v ...interface{}) { - m := fmt.Sprintf(msg, v...) - log.Printf(LogFatal, r.Method, r.URL, m) - writeErr(w, r, m, http.StatusInternalServerError) -} diff --git a/pkg/httputil/httperrors_test.go b/pkg/httputil/httperrors_test.go deleted file mode 100644 index ea1fecad9..000000000 --- a/pkg/httputil/httperrors_test.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package httputil - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestNotFound(t *testing.T) { - fn := func(w http.ResponseWriter, r *http.Request) { - NotFound(w, r) - } - testStatusCode(http.HandlerFunc(fn), 404, t) -} - -func TestFatal(t *testing.T) { - fn := func(w http.ResponseWriter, r *http.Request) { - Fatal(w, r, "fatal %s", "foo") - } - testStatusCode(http.HandlerFunc(fn), 500, t) -} - -func testStatusCode(fn http.HandlerFunc, expect int, t *testing.T) { - s := httptest.NewServer(fn) - defer s.Close() - - res, err := http.Get(s.URL) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != expect { - t.Errorf("Expected %d, got %d", expect, res.StatusCode) - } -} diff --git a/pkg/kubectl/cluster_info.go b/pkg/kubectl/cluster_info.go deleted file mode 100644 index 7bcebacb0..000000000 --- a/pkg/kubectl/cluster_info.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -// ClusterInfo returns Kubernetes cluster info -func (r RealRunner) ClusterInfo() ([]byte, error) { - return command("cluster-info").CombinedOutput() -} - -// ClusterInfo returns the commands to kubectl -func (r PrintRunner) ClusterInfo() ([]byte, error) { - cmd := command("cluster-info") - return []byte(cmd.String()), nil -} diff --git a/pkg/kubectl/command.go b/pkg/kubectl/command.go deleted file mode 100644 index 2698bb6a8..000000000 --- a/pkg/kubectl/command.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -import ( - "bytes" - "fmt" - "io/ioutil" - "os/exec" - "strings" -) - -type cmd struct { - *exec.Cmd -} - -func command(args ...string) *cmd { - return &cmd{exec.Command(Path, args...)} -} - -func assignStdin(cmd *cmd, in []byte) { - fmt.Println(string(in)) - cmd.Stdin = bytes.NewBuffer(in) -} - -func (c *cmd) String() string { - var stdin string - - if c.Stdin != nil { - b, _ := ioutil.ReadAll(c.Stdin) - stdin = fmt.Sprintf("< %s", string(b)) - } - - return fmt.Sprintf("[CMD] %s %s", strings.Join(c.Args, " "), stdin) -} diff --git a/pkg/kubectl/create.go b/pkg/kubectl/create.go deleted file mode 100644 index 379d4144b..000000000 --- a/pkg/kubectl/create.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -// Create uploads a chart to Kubernetes -func (r RealRunner) Create(stdin []byte) ([]byte, error) { - args := []string{"create", "-f", "-"} - - cmd := command(args...) - assignStdin(cmd, stdin) - - return cmd.CombinedOutput() -} - -// Create returns the commands to kubectl -func (r PrintRunner) Create(stdin []byte) ([]byte, error) { - args := []string{"create", "-f", "-"} - - cmd := command(args...) - assignStdin(cmd, stdin) - - return []byte(cmd.String()), nil -} diff --git a/pkg/kubectl/create_test.go b/pkg/kubectl/create_test.go deleted file mode 100644 index 078eab0a2..000000000 --- a/pkg/kubectl/create_test.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -import ( - "testing" -) - -func TestPrintCreate(t *testing.T) { - var client Runner = PrintRunner{} - - expected := `[CMD] kubectl create -f - < some stdin data` - - out, err := client.Create([]byte("some stdin data")) - if err != nil { - t.Error(err) - } - - actual := string(out) - - if expected != actual { - t.Fatalf("actual %s != expected %s", actual, expected) - } -} diff --git a/pkg/kubectl/delete.go b/pkg/kubectl/delete.go deleted file mode 100644 index 71e57b0e6..000000000 --- a/pkg/kubectl/delete.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -// Delete removes a chart from Kubernetes. -func (r RealRunner) Delete(name, ktype string) ([]byte, error) { - - args := []string{"delete", ktype, name} - - return command(args...).CombinedOutput() -} - -// Delete returns the commands to kubectl -func (r PrintRunner) Delete(name, ktype string) ([]byte, error) { - - args := []string{"delete", ktype, name} - - cmd := command(args...) - return []byte(cmd.String()), nil -} diff --git a/pkg/kubectl/get.go b/pkg/kubectl/get.go deleted file mode 100644 index e618ff6c4..000000000 --- a/pkg/kubectl/get.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -// Get returns Kubernetes resources -func (r RealRunner) Get(stdin []byte, ns string) ([]byte, error) { - args := []string{"get", "-f", "-"} - - if ns != "" { - args = append([]string{"--namespace=" + ns}, args...) - } - cmd := command(args...) - assignStdin(cmd, stdin) - - return cmd.CombinedOutput() -} - -// GetByKind gets resources by kind, name(optional), and namespace(optional) -func (r RealRunner) GetByKind(kind, name, ns string) (string, error) { - args := []string{"get", kind} - - if name != "" { - args = append(args, name) - } - - if ns != "" { - args = append([]string{"--namespace=" + ns}, args...) - } - cmd := command(args...) - o, err := cmd.CombinedOutput() - return string(o), err -} - -// Get returns the commands to kubectl -func (r PrintRunner) Get(stdin []byte, ns string) ([]byte, error) { - args := []string{"get", "-f", "-"} - - if ns != "" { - args = append([]string{"--namespace=" + ns}, args...) - } - cmd := command(args...) - assignStdin(cmd, stdin) - - return []byte(cmd.String()), nil -} - -// GetByKind gets resources by kind, name(optional), and namespace(optional) -func (r PrintRunner) GetByKind(kind, name, ns string) (string, error) { - args := []string{"get", kind} - - if name != "" { - args = append([]string{name}, args...) - } - - if ns != "" { - args = append([]string{"--namespace=" + ns}, args...) - } - cmd := command(args...) - return cmd.String(), nil -} diff --git a/pkg/kubectl/get_test.go b/pkg/kubectl/get_test.go deleted file mode 100644 index e82f7b6e1..000000000 --- a/pkg/kubectl/get_test.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -import ( - "testing" -) - -func TestGet(t *testing.T) { - Client = TestRunner{ - out: []byte("running the get command"), - } - - expects := "running the get command" - out, _ := Client.Get([]byte{}, "") - if string(out) != expects { - t.Errorf("%s != %s", string(out), expects) - } -} - -func TestGetByKind(t *testing.T) { - Client = TestRunner{ - out: []byte("running the GetByKind command"), - } - - expects := "running the GetByKind command" - out, _ := Client.GetByKind("pods", "", "") - if out != expects { - t.Errorf("%s != %s", out, expects) - } -} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go deleted file mode 100644 index c83da8c39..000000000 --- a/pkg/kubectl/kubectl.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -// Path is the path of the kubectl binary -var Path = "kubectl" - -// Runner is an interface to wrap kubectl convenience methods -type Runner interface { - // ClusterInfo returns Kubernetes cluster info - ClusterInfo() ([]byte, error) - // Create uploads a chart to Kubernetes - Create(stdin []byte) ([]byte, error) - // Delete removes a chart from Kubernetes. - Delete(name string, ktype string) ([]byte, error) - // Get returns Kubernetes resources - Get(stdin []byte, ns string) ([]byte, error) - - // GetByKind gets an entry by kind, name, and namespace. - // - // If name is omitted, all entries of that kind are returned. - // - // If NS is omitted, the default NS is assumed. - GetByKind(kind, name, ns string) (string, error) -} - -// RealRunner implements Runner to execute kubectl commands -type RealRunner struct{} - -// PrintRunner implements Runner to return a []byte of the command to be executed -type PrintRunner struct{} - -// Client stores the instance of Runner -var Client Runner = RealRunner{} diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go deleted file mode 100644 index 4b7579577..000000000 --- a/pkg/kubectl/kubectl_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package kubectl - -type TestRunner struct { - Runner - - out []byte - err error -} - -func (r TestRunner) Get(stdin []byte, ns string) ([]byte, error) { - return r.out, r.err -} - -func (r TestRunner) GetByKind(kind, name, ns string) (string, error) { - return string(r.out), r.err -} diff --git a/pkg/log/log.go b/pkg/log/log.go deleted file mode 100644 index 269659cb0..000000000 --- a/pkg/log/log.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -/* -Package log provides simple convenience wrappers for logging. - -Following convention, this provides functions for logging warnings, errors, information -and debugging. -*/ -package log - -import ( - "log" - "os" -) - -// Receiver can receive log messages from this package. -type Receiver interface { - Printf(format string, v ...interface{}) -} - -// Logger is the destination for this package. -// -// The logger that this prints to. -var Logger Receiver = log.New(os.Stderr, "", log.LstdFlags) - -// IsDebugging controls debugging output. -// -// If this is true, debugging messages will be printed. Expensive debugging -// operations can be wrapped in `if log.IsDebugging {}`. -var IsDebugging = false - -// Err prints an error of severity ERROR to the log. -func Err(msg string, v ...interface{}) { - Logger.Printf("[ERROR] "+msg+"\n", v...) -} - -// Warn prints an error severity WARN to the log. -func Warn(msg string, v ...interface{}) { - Logger.Printf("[WARN] "+msg+"\n", v...) -} - -// Info prints an error of severity INFO to the log. -func Info(msg string, v ...interface{}) { - Logger.Printf("[INFO] "+msg+"\n", v...) -} - -// Debug prints an error severity DEBUG to the log. -// -// Debug will only print if IsDebugging is true. -func Debug(msg string, v ...interface{}) { - if IsDebugging { - Logger.Printf("[DEBUG] "+msg+"\n", v...) - } -} diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go deleted file mode 100644 index 9d8a03a0a..000000000 --- a/pkg/log/log_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package log - -import ( - "bytes" - "fmt" - "testing" -) - -type LoggerMock struct { - b bytes.Buffer -} - -func (l *LoggerMock) Printf(m string, v ...interface{}) { - l.b.Write([]byte(fmt.Sprintf(m, v...))) -} - -func TestLogger(t *testing.T) { - l := &LoggerMock{} - Logger = l - IsDebugging = true - - Err("%s%s%s", "a", "b", "c") - expect := "[ERROR] abc\n" - if l.b.String() != expect { - t.Errorf("Expected %q, got %q", expect, l.b.String()) - } - l.b.Reset() - - tests := map[string]func(string, ...interface{}){ - "[WARN] test\n": Warn, - "[INFO] test\n": Info, - "[DEBUG] test\n": Debug, - } - - for expect, f := range tests { - f("test") - if l.b.String() != expect { - t.Errorf("Expected %q, got %q", expect, l.b.String()) - } - l.b.Reset() - } - - IsDebugging = false - Debug("HELLO") - if l.b.String() != "" { - t.Errorf("Expected debugging to disable. Got %q", l.b.String()) - } - l.b.Reset() -} diff --git a/pkg/pkg_test.go b/pkg/pkg_test.go deleted file mode 100644 index ef3c61346..000000000 --- a/pkg/pkg_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package pkg - -import ( - "testing" - "text/template" -) - -// TestGoFeatures is a canary test to make sure that features that are invisible at API level are supported. -func TestGoFeatures(t *testing.T) { - // Test that template with Go 1.6 syntax can compile. - _, err := template.New("test").Parse(`{{- printf "hello"}}`) - if err != nil { - t.Fatalf("You must use a version of Go that supports {{- template syntax. (1.6+)") - } -} diff --git a/pkg/repo/filebased_credential_provider.go b/pkg/repo/filebased_credential_provider.go deleted file mode 100644 index 65077030c..000000000 --- a/pkg/repo/filebased_credential_provider.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "github.com/ghodss/yaml" - - "fmt" - "io/ioutil" - "log" -) - -// FilebasedCredentialProvider provides credentials for registries. -type FilebasedCredentialProvider struct { - // Actual backing store - backingCredentialProvider ICredentialProvider -} - -// NamedRepoCredential associates a name with a Credential. -type NamedRepoCredential struct { - Name string `json:"name,omitempty"` - Credential -} - -// NewFilebasedCredentialProvider creates a file based credential provider. -func NewFilebasedCredentialProvider(filename string) (ICredentialProvider, error) { - icp := NewInmemCredentialProvider() - log.Printf("Using credentials file %s", filename) - c, err := readCredentialsFile(filename) - if err != nil { - return nil, err - } - - for _, nc := range c { - log.Printf("Loading credential named %s", nc.Name) - icp.SetCredential(nc.Name, &nc.Credential) - } - - return &FilebasedCredentialProvider{icp}, nil -} - -func readCredentialsFile(filename string) ([]NamedRepoCredential, error) { - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - return parseCredentials(bytes) -} - -func parseCredentials(bytes []byte) ([]NamedRepoCredential, error) { - r := []NamedRepoCredential{} - if err := yaml.Unmarshal(bytes, &r); err != nil { - return nil, fmt.Errorf("cannot unmarshal credentials file (%#v)", err) - } - - return r, nil -} - -// GetCredential returns a credential by name. -func (fcp *FilebasedCredentialProvider) GetCredential(name string) (*Credential, error) { - return fcp.backingCredentialProvider.GetCredential(name) -} - -// SetCredential sets a credential by name. -func (fcp *FilebasedCredentialProvider) SetCredential(name string, credential *Credential) error { - return fmt.Errorf("SetCredential operation not supported with FilebasedCredentialProvider") -} diff --git a/pkg/repo/filebased_credential_provider_test.go b/pkg/repo/filebased_credential_provider_test.go deleted file mode 100644 index 89620ca07..000000000 --- a/pkg/repo/filebased_credential_provider_test.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "testing" -) - -var filename = "./testdata/test_credentials_file.yaml" - -type filebasedTestCase struct { - name string - exp *Credential - expErr error -} - -func TestNotExistFilebased(t *testing.T) { - cp := getProvider(t) - tc := &testCase{"nonexistent", nil, createMissingError("nonexistent")} - testGetCredential(t, cp, tc) -} - -func TestGetApiTokenFilebased(t *testing.T) { - cp := getProvider(t) - tc := &testCase{"test1", &Credential{APIToken: "token"}, nil} - testGetCredential(t, cp, tc) -} - -func TestSetAndGetBasicAuthFilebased(t *testing.T) { - cp := getProvider(t) - ba := BasicAuthCredential{Username: "user", Password: "password"} - tc := &testCase{"test2", &Credential{BasicAuth: ba}, nil} - testGetCredential(t, cp, tc) -} - -func getProvider(t *testing.T) ICredentialProvider { - cp, err := NewFilebasedCredentialProvider(filename) - if err != nil { - t.Fatalf("cannot create a new provider from file %s: %s", filename, err) - } - - return cp -} diff --git a/pkg/repo/gcs_repo.go b/pkg/repo/gcs_repo.go deleted file mode 100644 index 26f285409..000000000 --- a/pkg/repo/gcs_repo.go +++ /dev/null @@ -1,191 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "github.com/kubernetes/helm/pkg/chart" - "github.com/kubernetes/helm/pkg/util" - - storage "google.golang.org/api/storage/v1" - - "fmt" - "net/http" - "net/url" - "regexp" - "strings" -) - -// GCSRepoURLMatcher matches the GCS repository URL format (gs://<bucket>). -var GCSRepoURLMatcher = regexp.MustCompile("gs://(.*)") - -// GCSChartURLMatcher matches the GCS chart URL format (gs://<bucket>/<name>-<version>.tgz). -var GCSChartURLMatcher = regexp.MustCompile("gs://(.*)/(.*)-(.*).tgz") - -const ( - // GCSRepoType identifies the GCS repository type. - GCSRepoType = ERepoType("gcs") - - // GCSRepoFormat identifies the GCS repository format. - // In a GCS repository all charts appear at the top level. - GCSRepoFormat = FlatRepoFormat - - // GCSPublicRepoBucket is the name of the public GCS repository bucket. - GCSPublicRepoBucket = "kubernetes-charts" - - // GCSPublicRepoURL is the URL for the public GCS repository. - GCSPublicRepoURL = "gs://" + GCSPublicRepoBucket -) - -// GCSRepo implements the IStorageRepo interface for Google Cloud Storage. -type GCSRepo struct { - Repo - bucket string - httpClient *http.Client - service *storage.Service -} - -// NewPublicGCSRepo creates a new an IStorageRepo for the public GCS repository. -func NewPublicGCSRepo(httpClient *http.Client) (*GCSRepo, error) { - return NewGCSRepo(GCSPublicRepoURL, "", GCSPublicRepoBucket, nil) -} - -// NewGCSRepo creates a new IStorageRepo for a given GCS repository. -func NewGCSRepo(URL, credentialName, repoName string, httpClient *http.Client) (*GCSRepo, error) { - r, err := newRepo(URL, credentialName, repoName, GCSRepoFormat, GCSRepoType) - if err != nil { - return nil, err - } - - return newGCSRepo(r, httpClient) -} - -func newGCSRepo(r *Repo, httpClient *http.Client) (*GCSRepo, error) { - URL := r.GetURL() - m := GCSRepoURLMatcher.FindStringSubmatch(URL) - if len(m) != 2 { - return nil, fmt.Errorf("URL must be of the form gs://<bucket>, was %s", URL) - } - - if err := validateRepoType(r.GetType()); err != nil { - return nil, err - } - - if httpClient == nil { - httpClient = http.DefaultClient - } - - gcs, err := storage.New(httpClient) - if err != nil { - return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err) - } - - gcsr := &GCSRepo{ - Repo: *r, - httpClient: httpClient, - service: gcs, - bucket: m[1], - } - - return gcsr, nil -} - -func validateRepoType(repoType ERepoType) error { - switch repoType { - case GCSRepoType: - return nil - } - - return fmt.Errorf("unknown repository type: %s", repoType) -} - -// ListCharts lists charts in this chart repository whose string values conform to the -// supplied regular expression, or all charts, if the regular expression is nil. -func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) { - charts := []string{} - - // ListRepos all objects in a bucket using pagination - pageToken := "" - for { - call := g.service.Objects.List(g.bucket) - call.Delimiter("/") - if pageToken != "" { - call = call.PageToken(pageToken) - } - - res, err := call.Do() - if err != nil { - return nil, err - } - - for _, object := range res.Items { - // Charts should be named chart-X.Y.Z.tgz, so tease apart the name - m := ChartNameMatcher.FindStringSubmatch(object.Name) - if len(m) != 3 { - continue - } - - if regex == nil || regex.MatchString(object.Name) { - charts = append(charts, object.Name) - } - } - - if pageToken = res.NextPageToken; pageToken == "" { - break - } - } - - return charts, nil -} - -// GetChart retrieves, unpacks and returns a chart by name. -func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) { - // Charts should be named chart-X.Y.Z.tgz, so check that the name matches - if !ChartNameMatcher.MatchString(name) { - return nil, fmt.Errorf("name must be of the form <name>-<version>.tgz, was %s", name) - } - - call := g.service.Objects.Get(g.bucket, name) - object, err := call.Do() - if err != nil { - return nil, fmt.Errorf("cannot get storage object named %s/%s: %s", g.bucket, name, err) - } - - u, err := url.Parse(object.MediaLink) - if err != nil { - return nil, fmt.Errorf("cannot parse URL %s for chart %s/%s: %s", - object.MediaLink, object.Bucket, object.Name, err) - } - - getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper()) - body, code, err := getter.Get(u.String()) - if err != nil { - return nil, fmt.Errorf("cannot fetch URL %s for chart %s/%s: %d %s", - object.MediaLink, object.Bucket, object.Name, code, err) - } - - return chart.LoadDataFromReader(strings.NewReader(body)) -} - -// GetBucket returns the repository bucket. -func (g *GCSRepo) GetBucket() string { - return g.bucket -} - -// Do performs an HTTP operation on the receiver's httpClient. -func (g *GCSRepo) Do(req *http.Request) (resp *http.Response, err error) { - return g.httpClient.Do(req) -} diff --git a/pkg/repo/gcs_repo_test.go b/pkg/repo/gcs_repo_test.go deleted file mode 100644 index fab4aea9d..000000000 --- a/pkg/repo/gcs_repo_test.go +++ /dev/null @@ -1,138 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "github.com/kubernetes/helm/pkg/chart" - - "os" - "reflect" - "regexp" - "testing" -) - -var ( - TestArchiveURL = os.Getenv("TEST_ARCHIVE_URL") - TestChartName = "frobnitz" - TestChartVersion = "0.0.1" - TestArchiveName = TestChartName + "-" + TestChartVersion + ".tgz" - TestChartFile = "../chart/testdata/frobnitz/Chart.yaml" - TestShouldFindRegex = regexp.MustCompile(TestArchiveName) - TestShouldNotFindRegex = regexp.MustCompile("foobar") - TestName = "bucket-name" -) - -func TestValidGSURL(t *testing.T) { - tr := getTestRepo(t) - err := validateRepo(tr, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType) - if err != nil { - t.Fatal(err) - } - - wantBucket := TestRepoBucket - haveBucket := tr.GetBucket() - if haveBucket != wantBucket { - t.Fatalf("unexpected bucket; want: %s, have %s.", wantBucket, haveBucket) - } -} - -func TestInvalidGSURL(t *testing.T) { - var invalidGSURL = "https://valid.url/wrong/scheme" - _, err := NewGCSRepo(invalidGSURL, TestRepoCredentialName, TestName, nil) - if err == nil { - t.Fatalf("expected error did not occur for invalid GS URL") - } -} - -func TestListCharts(t *testing.T) { - tr := getTestRepo(t) - charts, err := tr.ListCharts(nil) - if err != nil { - t.Fatal(err) - } - - if len(charts) < 1 { - t.Fatalf("expected at least one chart in test repository %s", TestRepoURL) - } - - for _, ch := range charts { - if ch == TestArchiveName { - return - } - } - - t.Fatalf("expected chart named %s in test repository %s", TestArchiveName, TestRepoURL) -} - -func TestListChartsWithShouldFindRegex(t *testing.T) { - tr := getTestRepo(t) - charts, err := tr.ListCharts(TestShouldFindRegex) - if err != nil { - t.Fatal(err) - } - - if len(charts) != 1 { - t.Fatalf("expected one chart to match regex, got %d", len(charts)) - } -} - -func TestListChartsWithShouldNotFindRegex(t *testing.T) { - tr := getTestRepo(t) - charts, err := tr.ListCharts(TestShouldNotFindRegex) - if err != nil { - t.Fatal(err) - } - - if len(charts) != 0 { - t.Fatalf("expected zero charts to match regex, got %d", len(charts)) - } -} - -func TestGetChart(t *testing.T) { - tr := getTestRepo(t) - tc, err := tr.GetChart(TestArchiveName) - if err != nil { - t.Fatal(err) - } - - haveFile := tc.Chartfile() - wantFile, err := chart.LoadChartfile(TestChartFile) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(wantFile, haveFile) { - t.Fatalf("retrieved invalid chart\nwant:%#v\nhave:\n%#v\n", wantFile, haveFile) - } -} - -func TestGetChartWithInvalidName(t *testing.T) { - tr := getTestRepo(t) - _, err := tr.GetChart("NotAValidArchiveName") - if err == nil { - t.Fatalf("found chart using invalid archive name") - } -} - -func getTestRepo(t *testing.T) IStorageRepo { - tr, err := NewGCSRepo(TestRepoURL, TestRepoCredentialName, TestName, nil) - if err != nil { - t.Fatal(err) - } - - return tr -} diff --git a/pkg/repo/inmem_credential_provider.go b/pkg/repo/inmem_credential_provider.go deleted file mode 100644 index 9d134e3ec..000000000 --- a/pkg/repo/inmem_credential_provider.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "fmt" - "sync" -) - -// InmemCredentialProvider is a memory based credential provider. -type InmemCredentialProvider struct { - sync.RWMutex - credentials map[string]*Credential -} - -// NewInmemCredentialProvider creates a new memory based credential provider. -func NewInmemCredentialProvider() ICredentialProvider { - return &InmemCredentialProvider{credentials: make(map[string]*Credential)} -} - -// GetCredential returns a credential by name. -func (fcp *InmemCredentialProvider) GetCredential(name string) (*Credential, error) { - fcp.RLock() - defer fcp.RUnlock() - - if val, ok := fcp.credentials[name]; ok { - return val, nil - } - - return nil, fmt.Errorf("no such credential: %s", name) -} - -// SetCredential sets a credential by name. -func (fcp *InmemCredentialProvider) SetCredential(name string, credential *Credential) error { - fcp.Lock() - defer fcp.Unlock() - - fcp.credentials[name] = &Credential{APIToken: credential.APIToken, BasicAuth: credential.BasicAuth, ServiceAccount: credential.ServiceAccount} - return nil -} diff --git a/pkg/repo/inmem_credential_provider_test.go b/pkg/repo/inmem_credential_provider_test.go deleted file mode 100644 index 64d642008..000000000 --- a/pkg/repo/inmem_credential_provider_test.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "fmt" - "reflect" - "testing" -) - -type testCase struct { - name string - exp *Credential - expErr error -} - -func createMissingError(name string) error { - return fmt.Errorf("no such credential: %s", name) -} - -func testGetCredential(t *testing.T, cp ICredentialProvider, tc *testCase) { - actual, actualErr := cp.GetCredential(tc.name) - if !reflect.DeepEqual(actual, tc.exp) { - t.Fatalf("test case %s failed: want: %#v, have: %#v", tc.name, tc.exp, actual) - } - - if !reflect.DeepEqual(actualErr, tc.expErr) { - t.Fatalf("test case %s failed: want: %s, have: %s", tc.name, tc.expErr, actualErr) - } -} - -func verifySetAndGetCredential(t *testing.T, cp ICredentialProvider, tc *testCase) { - err := cp.SetCredential(tc.name, tc.exp) - if err != nil { - t.Fatalf("test case %s failed: cannot set credential: %v", tc.name, err) - } - - testGetCredential(t, cp, tc) -} - -func TestNotExist(t *testing.T) { - cp := NewInmemCredentialProvider() - tc := &testCase{"nonexistent", nil, createMissingError("nonexistent")} - testGetCredential(t, cp, tc) -} - -func TestSetAndGetApiToken(t *testing.T) { - cp := NewInmemCredentialProvider() - tc := &testCase{"testcredential", &Credential{APIToken: "some token here"}, nil} - verifySetAndGetCredential(t, cp, tc) -} - -func TestSetAndGetBasicAuth(t *testing.T) { - cp := NewInmemCredentialProvider() - ba := BasicAuthCredential{Username: "user", Password: "pass"} - tc := &testCase{"testcredential", &Credential{BasicAuth: ba}, nil} - verifySetAndGetCredential(t, cp, tc) -} diff --git a/pkg/repo/inmem_repo_service.go b/pkg/repo/inmem_repo_service.go deleted file mode 100644 index 1df1162c6..000000000 --- a/pkg/repo/inmem_repo_service.go +++ /dev/null @@ -1,156 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "errors" - "fmt" - "strings" - "sync" -) - -type inmemRepoService struct { - sync.RWMutex - repositories map[string]IRepo -} - -// NewInmemRepoService returns a new memory based repository service. -func NewInmemRepoService() IRepoService { - rs := &inmemRepoService{ - repositories: make(map[string]IRepo), - } - - r, err := NewPublicGCSRepo(nil) - if err == nil { - rs.CreateRepo(r) - } - - return rs -} - -// ListRepos returns the list of all known chart repositories -func (rs *inmemRepoService) ListRepos() (map[string]string, error) { - rs.RLock() - defer rs.RUnlock() - - ret := make(map[string]string) - for _, r := range rs.repositories { - ret[r.GetName()] = r.GetURL() - } - - return ret, nil -} - -// CreateRepo adds a known repository to the list -func (rs *inmemRepoService) CreateRepo(repository IRepo) error { - rs.Lock() - defer rs.Unlock() - - URL := repository.GetURL() - name := repository.GetName() - - valid := GCSRepoURLMatcher.MatchString(URL) - if !valid { - return errors.New(URL + " is an invalid Repo URL") - } - - for u, r := range rs.repositories { - if u == URL { - return fmt.Errorf("Repository with URL %s already exists", URL) - } else if r.GetName() == name { - return fmt.Errorf("Repository with Name %s already exists", name) - } - } - - rs.repositories[URL] = repository - return nil -} - -// GetRepoByURL returns the repository with the given URL -func (rs *inmemRepoService) GetRepoByURL(URL string) (IRepo, error) { - rs.RLock() - defer rs.RUnlock() - - r, ok := rs.repositories[URL] - if !ok { - return nil, fmt.Errorf("No repository with URL %s", URL) - } - - return r, nil -} - -// GetRepoByChartURL returns the repository that backs the given chart URL -func (rs *inmemRepoService) GetRepoByChartURL(URL string) (IRepo, error) { - rs.RLock() - defer rs.RUnlock() - - cSplit := strings.Split(URL, "/") - var found IRepo - for _, r := range rs.repositories { - rURL := r.GetURL() - rSplit := strings.Split(rURL, "/") - if hasPrefix(cSplit, rSplit) { - if found == nil || len(found.GetURL()) < len(rURL) { - found = r - } - } - } - - if found == nil { - return nil, fmt.Errorf("No repository found for chart url: %s", URL) - } - - return found, nil -} - -func hasPrefix(cSplit, rSplit []string) bool { - if len(rSplit) > len(cSplit) { - return false - } - - for i := range rSplit { - if rSplit[i] != cSplit[i] { - return false - } - } - - return true -} - -// DeleteRepo removes a known repository from the list -func (rs *inmemRepoService) DeleteRepo(URL string) error { - rs.Lock() - defer rs.Unlock() - - _, ok := rs.repositories[URL] - if !ok { - return fmt.Errorf("No repository with URL %s", URL) - } - - delete(rs.repositories, URL) - return nil -} - -func (rs *inmemRepoService) GetRepoURLByName(name string) (string, error) { - for url, r := range rs.repositories { - if r.GetName() == name { - return url, nil - } - } - err := fmt.Errorf("No repository url found with name %s", name) - return "", err -} diff --git a/pkg/repo/inmem_repo_service_test.go b/pkg/repo/inmem_repo_service_test.go deleted file mode 100644 index 119c8535f..000000000 --- a/pkg/repo/inmem_repo_service_test.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "reflect" - "testing" -) - -func TestService(t *testing.T) { - rs := NewInmemRepoService() - repos, err := rs.ListRepos() - if err != nil { - t.Fatal(err) - } - - if len(repos) != 1 { - t.Fatalf("unexpected repo count; want: %d, have %d.", 1, len(repos)) - } - - u := "" - for _, url := range repos { - u = url - } - tr, err := rs.GetRepoByURL(u) - if err != nil { - t.Fatal(err) - } - - if err := validateRepo(tr, GCSPublicRepoURL, "", GCSRepoFormat, GCSRepoType); err != nil { - t.Fatal(err) - } - - r1, err := rs.GetRepoByURL(GCSPublicRepoURL) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(r1, tr) { - t.Fatalf("invalid repo returned; want: %#v, have %#v.", tr, r1) - } - - URL := GCSPublicRepoURL + "/" + TestArchiveName - r2, err := rs.GetRepoByChartURL(URL) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(r2, tr) { - t.Fatalf("invalid repo returned; want: %#v, have %#v.", tr, r2) - } - - if err := rs.DeleteRepo(GCSPublicRepoURL); err != nil { - t.Fatal(err) - } - - if _, err := rs.GetRepoByURL(GCSPublicRepoURL); err == nil { - t.Fatalf("deleted repo with URL %s returned", GCSPublicRepoURL) - } -} - -func TestCreateRepoWithDuplicateURL(t *testing.T) { - rs := NewInmemRepoService() - r, err := newRepo(GCSPublicRepoURL, "", TestName, GCSRepoFormat, GCSRepoType) - if err != nil { - t.Fatalf("cannot create test repo: %s", err) - } - - if err := rs.CreateRepo(r); err == nil { - t.Fatalf("created repo with duplicate URL: %s", GCSPublicRepoURL) - } -} - -func TestCreateRepoWithInvalidURL(t *testing.T) { - rs := NewInmemRepoService() - invalidURL := "fake://sfds" - r, err := newRepo(invalidURL, "", TestName, GCSRepoFormat, GCSRepoType) - if err != nil { - t.Fatalf("cannot create test repo: %v", err) - } - - if err = rs.CreateRepo(r); err == nil { - t.Fatalf("created repo with invalid URL: %s", invalidURL) - } -} - -func TestGetRepoWithInvalidURL(t *testing.T) { - invalidURL := "https://not.a.valid/url" - rs := NewInmemRepoService() - _, err := rs.GetRepoByURL(invalidURL) - if err == nil { - t.Fatalf("found repo with invalid URL: %s", invalidURL) - } -} - -func TestGetRepoURLByName(t *testing.T) { - rs := NewInmemRepoService() - testURL := "gs://helm-test-charts" - r, err := newRepo(testURL, "", TestName, GCSRepoFormat, GCSRepoType) - err = rs.CreateRepo(r) - if err != nil { - t.Fatalf("Error creating repo: %s", err) - } - expectedURL := testURL - actualURL, err := rs.GetRepoURLByName(TestName) - if err != nil { - t.Fatalf("%v", err) - } - if expectedURL != actualURL { - t.Fatalf("Incorrect resulting URL. Expected %s. Got %s", expectedURL, actualURL) - } - -} - -func TestGetRepoWithInvalidChartURL(t *testing.T) { - invalidURL := "https://not.a.valid/url" - rs := NewInmemRepoService() - _, err := rs.GetRepoByChartURL(invalidURL) - if err == nil { - t.Fatalf("found repo with invalid chart URL: %s", invalidURL) - } -} - -func TestDeleteRepoWithInvalidURL(t *testing.T) { - invalidURL := "https://not.a.valid/url" - rs := NewInmemRepoService() - err := rs.DeleteRepo(invalidURL) - if err == nil { - t.Fatalf("deleted repo with invalid name: %s", invalidURL) - } -} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go deleted file mode 100644 index ce2a29cb3..000000000 --- a/pkg/repo/repo.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "fmt" - "net/url" -) - -// NewRepo takes params and returns a IRepo -func NewRepo(URL, credentialName, repoName, repoFormat, repoType string) (IRepo, error) { - return newRepo(URL, credentialName, repoName, ERepoFormat(repoFormat), ERepoType(repoType)) -} - -func newRepo(URL, credentialName, repoName string, repoFormat ERepoFormat, repoType ERepoType) (*Repo, error) { - _, err := url.Parse(URL) - if err != nil { - return nil, fmt.Errorf("invalid URL (%s): %s", URL, err) - } - - if credentialName == "" { - credentialName = "default" - } - - if err := validateRepoFormat(repoFormat); err != nil { - return nil, err - } - - r := &Repo{ - Name: repoName, - URL: URL, - CredentialName: credentialName, - Type: repoType, - Format: repoFormat, - } - - return r, nil -} - -// Currently, only flat repositories are supported. -func validateRepoFormat(repoFormat ERepoFormat) error { - switch repoFormat { - case FlatRepoFormat: - return nil - } - - return fmt.Errorf("unknown repository format: %s", repoFormat) -} - -// GetType returns the technology implementing this repository. -func (r *Repo) GetType() ERepoType { - return r.Type -} - -// GetName returns the name of this repository. -func (r *Repo) GetName() string { - return r.Name -} - -// GetURL returns the URL to the root of this repository. -func (r *Repo) GetURL() string { - return r.URL -} - -// GetFormat returns the format of this repository. -func (r *Repo) GetFormat() ERepoFormat { - return r.Format -} - -// GetCredentialName returns the credential name used to access this repository. -func (r *Repo) GetCredentialName() string { - return r.CredentialName -} - -func validateRepo(tr IRepo, wantURL, wantCredentialName string, wantFormat ERepoFormat, wantType ERepoType) error { - haveURL := tr.GetURL() - if haveURL != wantURL { - return fmt.Errorf("unexpected repository url; want: %s, have %s", wantURL, haveURL) - } - - haveCredentialName := tr.GetCredentialName() - if wantCredentialName == "" { - wantCredentialName = "default" - } - - if haveCredentialName != wantCredentialName { - return fmt.Errorf("unexpected repository credential name; want: %s, have %s", wantCredentialName, haveCredentialName) - } - - haveFormat := tr.GetFormat() - if haveFormat != wantFormat { - return fmt.Errorf("unexpected repository format; want: %s, have %s", wantFormat, haveFormat) - } - - haveType := tr.GetType() - if haveType != wantType { - return fmt.Errorf("unexpected repository type; want: %s, have %s", wantType, haveType) - } - - return nil -} diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go deleted file mode 100644 index 1415f7332..000000000 --- a/pkg/repo/repo_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "testing" -) - -var ( - TestRepoBucket = "kubernetes-charts-testing" - TestRepoURL = "gs://" + TestRepoBucket - TestRepoType = GCSRepoType - TestRepoFormat = GCSRepoFormat - TestRepoCredentialName = "default" -) - -func TestValidRepoURL(t *testing.T) { - tr, err := NewRepo(TestRepoURL, TestRepoCredentialName, TestRepoBucket, string(TestRepoFormat), string(TestRepoType)) - if err != nil { - t.Fatal(err) - } - - if err := validateRepo(tr, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType); err != nil { - t.Fatal(err) - } -} - -func TestInvalidRepoURL(t *testing.T) { - _, err := newRepo("%:invalid&url:%", TestRepoCredentialName, TestRepoBucket, TestRepoFormat, TestRepoType) - if err == nil { - t.Fatalf("expected error did not occur for invalid URL") - } -} - -func TestDefaultCredentialName(t *testing.T) { - tr, err := newRepo(TestRepoURL, "", TestRepoBucket, TestRepoFormat, TestRepoType) - if err != nil { - t.Fatalf("cannot create repo using default credential name") - } - - TestRepoCredentialName := "default" - haveCredentialName := tr.GetCredentialName() - if haveCredentialName != TestRepoCredentialName { - t.Fatalf("unexpected credential name; want: %s, have %s.", TestRepoCredentialName, haveCredentialName) - } -} - -func TestInvalidRepoFormat(t *testing.T) { - _, err := newRepo(TestRepoURL, TestRepoCredentialName, TestRepoBucket, "", TestRepoType) - if err == nil { - t.Fatalf("expected error did not occur for invalid format") - } -} diff --git a/pkg/repo/repoprovider.go b/pkg/repo/repoprovider.go deleted file mode 100644 index 360810613..000000000 --- a/pkg/repo/repoprovider.go +++ /dev/null @@ -1,258 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "github.com/kubernetes/helm/pkg/chart" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - storage "google.golang.org/api/storage/v1" - - "fmt" - "log" - "net/http" - "sync" -) - -type repoProvider struct { - sync.RWMutex - rs IRepoService - cp ICredentialProvider - gcsrp IGCSRepoProvider - repos map[string]IChartRepo -} - -// NewRepoProvider creates a new repository provider. -func NewRepoProvider(rs IRepoService, gcsrp IGCSRepoProvider, cp ICredentialProvider) IRepoProvider { - return newRepoProvider(rs, gcsrp, cp) -} - -// newRepoProvider creates a new repository provider. -func newRepoProvider(rs IRepoService, gcsrp IGCSRepoProvider, cp ICredentialProvider) *repoProvider { - if rs == nil { - rs = NewInmemRepoService() - } - - if cp == nil { - cp = NewInmemCredentialProvider() - } - - if gcsrp == nil { - gcsrp = NewGCSRepoProvider(cp) - } - - repos := make(map[string]IChartRepo) - rp := &repoProvider{rs: rs, gcsrp: gcsrp, cp: cp, repos: repos} - return rp -} - -// GetRepoService returns the repository service used by this repository provider. -func (rp *repoProvider) GetRepoService() IRepoService { - return rp.rs -} - -// GetCredentialProvider returns the credential provider used by this repository provider. -func (rp *repoProvider) GetCredentialProvider() ICredentialProvider { - return rp.cp -} - -// GetGCSRepoProvider returns the GCS repository provider used by this repository provider. -func (rp *repoProvider) GetGCSRepoProvider() IGCSRepoProvider { - return rp.gcsrp -} - -// GetRepoByURL returns the repository with the given name. -func (rp *repoProvider) GetRepoByURL(URL string) (IChartRepo, error) { - rp.Lock() - defer rp.Unlock() - - if r, ok := rp.repos[URL]; ok { - return r, nil - } - - cr, err := rp.rs.GetRepoByURL(URL) - if err != nil { - return nil, err - } - - return rp.createRepoByType(cr) -} - -func (rp *repoProvider) createRepoByType(r IRepo) (IChartRepo, error) { - switch r.GetType() { - case GCSRepoType: - cr, err := rp.gcsrp.GetGCSRepo(r) - if err != nil { - return nil, err - } - - return rp.createRepo(cr) - } - - return nil, fmt.Errorf("unknown repository type: %s", r.GetType()) -} - -func (rp *repoProvider) createRepo(cr IChartRepo) (IChartRepo, error) { - URL := cr.GetURL() - if _, ok := rp.repos[URL]; ok { - return nil, fmt.Errorf("respository with URL %s already exists", URL) - } - - rp.repos[URL] = cr - return cr, nil -} - -// GetRepoByChartURL returns the repository that backs a given chart URL. -func (rp *repoProvider) GetRepoByChartURL(URL string) (IChartRepo, error) { - rp.Lock() - defer rp.Unlock() - - cr, err := rp.rs.GetRepoByChartURL(URL) - if err != nil { - return nil, err - } - - if r, ok := rp.repos[cr.GetURL()]; ok { - return r, nil - } - - return rp.createRepoByType(cr) -} - -// GetChartByReference maps the supplied chart reference into a fully qualified -// URL, uses the URL to find the repository it references, queries the repository -// for the chart, and then returns the chart and the repository that backs it. -func (rp *repoProvider) GetChartByReference(reference string) (*chart.Chart, IChartRepo, error) { - l, URL, err := parseChartReference(reference) - if err != nil { - return nil, nil, err - } - - r, err := rp.GetRepoByChartURL(URL) - if err != nil { - return nil, nil, err - } - - name := fmt.Sprintf("%s-%s.tgz", l.Name, l.Version) - c, err := r.GetChart(name) - if err != nil { - return nil, nil, err - } - - return c, r, nil -} - -// IsChartReference returns true if the supplied string is a reference to a chart in a repository -func IsChartReference(reference string) bool { - if _, err := ParseChartReference(reference); err != nil { - return false - } - - return true -} - -// ParseChartReference parses a reference to a chart in a repository and returns the URL for the chart -func ParseChartReference(reference string) (*chart.Locator, error) { - l, _, err := parseChartReference(reference) - if err != nil { - return nil, err - } - - return l, nil -} - -func parseChartReference(reference string) (*chart.Locator, string, error) { - l, err := chart.Parse(reference) - if err != nil { - return nil, "", fmt.Errorf("cannot parse chart reference %s: %s", reference, err) - } - - URL, err := l.Long(true) - if err != nil { - return nil, "", fmt.Errorf("chart reference %s does not resolve to a URL: %s", reference, err) - } - - return l, URL, nil -} - -type gcsRepoProvider struct { - cp ICredentialProvider -} - -// NewGCSRepoProvider creates a IGCSRepoProvider. -func NewGCSRepoProvider(cp ICredentialProvider) IGCSRepoProvider { - if cp == nil { - cp = NewInmemCredentialProvider() - } - - return gcsRepoProvider{cp: cp} -} - -// GetGCSRepo returns a new Google Cloud Storage repository. If a credential is specified, it will try to -// fetch it and use it, and if the credential isn't found, it will fall back to an unauthenticated client. -func (gcsrp gcsRepoProvider) GetGCSRepo(r IRepo) (IStorageRepo, error) { - client, err := gcsrp.createGCSClient(r.GetCredentialName()) - if err != nil { - return nil, err - } - - return NewGCSRepo(r.GetURL(), r.GetCredentialName(), r.GetName(), client) -} - -func (gcsrp gcsRepoProvider) createGCSClient(credentialName string) (*http.Client, error) { - if credentialName == "" { - return http.DefaultClient, nil - } - - c, err := gcsrp.cp.GetCredential(credentialName) - if err != nil { - log.Printf("credential named %s not found: %s", credentialName, err) - log.Print("falling back to the default client") - return http.DefaultClient, nil - } - - config, err := google.JWTConfigFromJSON([]byte(c.ServiceAccount), storage.DevstorageReadOnlyScope) - if err != nil { - log.Fatalf("cannot parse client secret file: %s", err) - } - - return config.Client(oauth2.NoContext), nil -} - -// IsGCSChartReference returns true if the supplied string is a reference to a chart in a GCS repository -func IsGCSChartReference(reference string) bool { - if _, err := ParseGCSChartReference(reference); err != nil { - return false - } - - return true -} - -// ParseGCSChartReference parses a reference to a chart in a GCS repository and returns the URL for the chart -func ParseGCSChartReference(reference string) (*chart.Locator, error) { - l, URL, err := parseChartReference(reference) - if err != nil { - return nil, err - } - - m := GCSChartURLMatcher.FindStringSubmatch(URL) - if len(m) != 4 { - return nil, fmt.Errorf("chart reference %s resolve to invalid URL: %s", reference, URL) - } - - return l, nil -} diff --git a/pkg/repo/repoprovider_test.go b/pkg/repo/repoprovider_test.go deleted file mode 100644 index 137c74a99..000000000 --- a/pkg/repo/repoprovider_test.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "github.com/kubernetes/helm/pkg/chart" - - "reflect" - "testing" -) - -var ( - TestShortReference = "helm:gs/" + TestRepoBucket + "/" + TestChartName + "#" + TestChartVersion - TestLongReference = TestRepoURL + "/" + TestArchiveName -) - -var ValidChartReferences = []string{ - TestShortReference, - TestLongReference, -} - -var InvalidChartReferences = []string{ - "gs://missing-chart-segment", - "https://not-a-gcs-url", - "file://local-chart-reference", -} - -func TestRepoProvider(t *testing.T) { - rp := NewRepoProvider(nil, nil, nil) - haveRepo, err := rp.GetRepoByURL(GCSPublicRepoURL) - if err != nil { - t.Fatal(err) - } - - if err := validateRepo(haveRepo, GCSPublicRepoURL, "", GCSRepoFormat, GCSRepoType); err != nil { - t.Fatal(err) - } - - castRepo, ok := haveRepo.(IStorageRepo) - if !ok { - t.Fatalf("invalid repo type, want: IStorageRepo, have: %T.", haveRepo) - } - - wantBucket := GCSPublicRepoBucket - haveBucket := castRepo.GetBucket() - if haveBucket != wantBucket { - t.Fatalf("unexpected bucket; want: %s, have %s.", wantBucket, haveBucket) - } - - wantRepo := haveRepo - URL := GCSPublicRepoURL + "/" + TestArchiveName - haveRepo, err = rp.GetRepoByChartURL(URL) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(wantRepo, haveRepo) { - t.Fatalf("retrieved invalid repo; want: %#v, have %#v.", haveRepo, wantRepo) - } -} - -func TestGetRepoByURLWithInvalidURL(t *testing.T) { - var invalidURL = "https://valid.url/wrong/scheme" - rp := NewRepoProvider(nil, nil, nil) - _, err := rp.GetRepoByURL(invalidURL) - if err == nil { - t.Fatalf("found repo using invalid URL: %s", invalidURL) - } -} - -func TestGetRepoByChartURLWithInvalidChartURL(t *testing.T) { - var invalidURL = "https://valid.url/wrong/scheme" - rp := NewRepoProvider(nil, nil, nil) - _, err := rp.GetRepoByChartURL(invalidURL) - if err == nil { - t.Fatalf("found repo using invalid chart URL: %s", invalidURL) - } -} - -func TestGetChartByReferenceWithValidReferences(t *testing.T) { - rp := getTestRepoProvider(t) - wantFile, err := chart.LoadChartfile(TestChartFile) - if err != nil { - t.Fatal(err) - } - - for _, vcr := range ValidChartReferences { - t.Logf("getting chart by reference: %s", vcr) - tc, _, err := rp.GetChartByReference(vcr) - if err != nil { - t.Error(err) - continue - } - - haveFile := tc.Chartfile() - if !reflect.DeepEqual(wantFile, haveFile) { - t.Fatalf("retrieved invalid chart\nwant:%#v\nhave:\n%#v\n", wantFile, haveFile) - } - } -} - -func getTestRepoProvider(t *testing.T) IRepoProvider { - rp := newRepoProvider(nil, nil, nil) - rs := rp.GetRepoService() - tr, err := newRepo(TestRepoURL, TestRepoCredentialName, TestName, TestRepoFormat, TestRepoType) - if err != nil { - t.Fatalf("cannot create test repository: %s", err) - } - - if err := rs.CreateRepo(tr); err != nil { - t.Fatalf("cannot initialize repository service: %s", err) - } - - return rp -} - -func TestGetChartByReferenceWithInvalidReferences(t *testing.T) { - rp := NewRepoProvider(nil, nil, nil) - for _, icr := range InvalidChartReferences { - _, _, err := rp.GetChartByReference(icr) - if err == nil { - t.Fatalf("found chart using invalid reference: %s", icr) - } - } -} - -func TestIsGCSChartReferenceWithValidReferences(t *testing.T) { - for _, vcr := range ValidChartReferences { - if !IsGCSChartReference(vcr) { - t.Fatalf("valid chart reference %s not accepted", vcr) - } - } -} - -func TestIsGCSChartReferenceWithInvalidReferences(t *testing.T) { - for _, icr := range InvalidChartReferences { - if IsGCSChartReference(icr) { - t.Fatalf("invalid chart reference %s accepted", icr) - } - } -} - -func TestParseGCSChartReferences(t *testing.T) { - for _, vcr := range ValidChartReferences { - if _, err := ParseGCSChartReference(vcr); err != nil { - t.Fatal(err) - } - } -} - -func TestParseGCSChartReferenceWithInvalidReferences(t *testing.T) { - for _, icr := range InvalidChartReferences { - if _, err := ParseGCSChartReference(icr); err == nil { - t.Fatalf("invalid chart reference %s parsed correctly", icr) - } - } -} diff --git a/pkg/repo/secrets_credential_provider.go b/pkg/repo/secrets_credential_provider.go deleted file mode 100644 index ffd029a3f..000000000 --- a/pkg/repo/secrets_credential_provider.go +++ /dev/null @@ -1,133 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "encoding/base64" - "encoding/json" - "flag" - "fmt" - "log" - - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/util" -) - -var ( - kubePath = flag.String("kubectl", "./kubectl", "The path to the kubectl binary.") - kubeService = flag.String("service", "", "The DNS name of the kubernetes service.") - kubeServer = flag.String("server", "", "The IP address and optional port of the kubernetes master.") - kubeInsecure = flag.Bool("insecure-skip-tls-verify", false, "Do not check the server's certificate for validity.") - kubeConfig = flag.String("config", "", "Path to a kubeconfig file.") - kubeCertAuth = flag.String("certificate-authority", "", "Path to a file for the certificate authority.") - kubeClientCert = flag.String("client-certificate", "", "Path to a client certificate file.") - kubeClientKey = flag.String("client-key", "", "Path to a client key file.") - kubeToken = flag.String("token", "", "A service account token.") - kubeUsername = flag.String("username", "", "The username to use for basic auth.") - kubePassword = flag.String("password", "", "The password to use for basic auth.") -) - -var kubernetesConfig *util.KubernetesConfig - -const secretType = "Secret" - -// SecretsCredentialProvider provides credentials for registries from Kubernertes secrets. -type SecretsCredentialProvider struct { - // Actual object that talks to secrets service. - k util.Kubernetes -} - -// NewSecretsCredentialProvider creates a new secrets credential provider. -func NewSecretsCredentialProvider() ICredentialProvider { - kubernetesConfig := &util.KubernetesConfig{ - KubePath: *kubePath, - KubeService: *kubeService, - KubeServer: *kubeServer, - KubeInsecure: *kubeInsecure, - KubeConfig: *kubeConfig, - KubeCertAuth: *kubeCertAuth, - KubeClientCert: *kubeClientCert, - KubeClientKey: *kubeClientKey, - KubeToken: *kubeToken, - KubeUsername: *kubeUsername, - KubePassword: *kubePassword, - } - return &SecretsCredentialProvider{util.NewKubernetesKubectl(kubernetesConfig)} -} - -func parseCredential(credential string) (*Credential, error) { - var c util.KubernetesSecret - if err := json.Unmarshal([]byte(credential), &c); err != nil { - return nil, fmt.Errorf("cannot unmarshal credential (%s): %s", credential, err) - } - - d, err := base64.StdEncoding.DecodeString(c.Data["credential"]) - if err != nil { - return nil, fmt.Errorf("cannot unmarshal credential (%s): %s", c, err) - } - - // And then finally unmarshal it from yaml to Credential - r := &Credential{} - if err := yaml.Unmarshal(d, &r); err != nil { - return nil, fmt.Errorf("cannot unmarshal credential %s (%#v)", c, err) - } - - return r, nil -} - -// GetCredential returns a credential by name. -func (scp *SecretsCredentialProvider) GetCredential(name string) (*Credential, error) { - o, err := scp.k.Get(name, secretType) - if err != nil { - return nil, err - } - - return parseCredential(o) -} - -// SetCredential sets a credential by name. -func (scp *SecretsCredentialProvider) SetCredential(name string, credential *Credential) error { - // Marshal the credential & base64 encode it. - b, err := yaml.Marshal(credential) - if err != nil { - log.Printf("yaml marshal failed for credential named %s: %s", name, err) - return err - } - - enc := base64.StdEncoding.EncodeToString(b) - - // Then create a kubernetes object out of it - metadata := make(map[string]string) - metadata["name"] = name - data := make(map[string]string) - data["credential"] = enc - obj := &util.KubernetesSecret{ - Kind: secretType, - APIVersion: "v1", - Metadata: metadata, - Data: data, - } - - ko, err := yaml.Marshal(obj) - if err != nil { - log.Printf("yaml marshal failed for kubernetes object named %s: %s", name, err) - return err - } - - _, err = scp.k.Create(string(ko)) - return err -} diff --git a/pkg/repo/testdata/test_credentials_file.yaml b/pkg/repo/testdata/test_credentials_file.yaml deleted file mode 100644 index 8ada52807..000000000 --- a/pkg/repo/testdata/test_credentials_file.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- name: test1 - apitoken: token -- name: test2 - basicauth: - username: user - password: password diff --git a/pkg/repo/types.go b/pkg/repo/types.go deleted file mode 100644 index 497f653cd..000000000 --- a/pkg/repo/types.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package repo - -import ( - "github.com/kubernetes/helm/pkg/chart" - - "regexp" -) - -// ChartNameMatcher matches the chart name format -var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz") - -// BasicAuthCredential holds a username and password. -type BasicAuthCredential struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// APITokenCredential defines an API token. -type APITokenCredential string - -// JWTTokenCredential defines a JWT token. -type JWTTokenCredential string - -// Credential holds a credential used to access a repository. -type Credential struct { - APIToken APITokenCredential `json:"apitoken,omitempty"` - BasicAuth BasicAuthCredential `json:"basicauth,omitempty"` - ServiceAccount JWTTokenCredential `json:"serviceaccount,omitempty"` -} - -// ICredentialProvider provides credentials for chart repositories. -type ICredentialProvider interface { - // SetCredential sets the credential for a repository. - // May not be supported by some repository services. - SetCredential(name string, credential *Credential) error - - // GetCredential returns the specified credential or nil if there's no credential. - // Error is non-nil if fetching the credential failed. - GetCredential(name string) (*Credential, error) -} - -// ERepoType defines the technology that implements a repository. -type ERepoType string - -// ERepoFormat is a semi-colon delimited string that describes the format of a repository. -type ERepoFormat string - -const ( - // PathRepoFormat identfies a repository where charts are organized hierarchically. - PathRepoFormat = ERepoFormat("path") - // FlatRepoFormat identifies a repository where all charts appear at the top level. - FlatRepoFormat = ERepoFormat("flat") -) - -// Repo describes a repository -type Repo struct { - Name string `json:"name"` // Name of repository - URL string `json:"url"` // URL to the root of this repository - CredentialName string `json:"credentialname,omitempty"` // Credential name used to access this repository - Format ERepoFormat `json:"format,omitempty"` // Format of this repository - Type ERepoType `json:"type,omitempty"` // Technology implementing this repository -} - -// IRepo abstracts a repository. -type IRepo interface { - // GetName returns the name of the repository - GetName() string - // GetURL returns the URL to the root of this repository. - GetURL() string - // GetCredentialName returns the credential name used to access this repository. - GetCredentialName() string - // GetFormat returns the format of this repository. - GetFormat() ERepoFormat - // GetType returns the technology implementing this repository. - GetType() ERepoType -} - -// IChartRepo abstracts a place that holds charts. -type IChartRepo interface { - // A IChartRepo is a IRepo - IRepo - - // ListCharts lists the URLs for charts in this repository that - // conform to the supplied regular expression, or all charts if regex is nil - ListCharts(regex *regexp.Regexp) ([]string, error) - - // GetChart retrieves, unpacks and returns a chart by name. - GetChart(name string) (*chart.Chart, error) -} - -// IStorageRepo abstracts a repository that resides in Object Storage, -// such as Google Cloud Storage, AWS S3, etc. -type IStorageRepo interface { - // An IStorageRepo is a IChartRepo - IChartRepo - - // GetBucket returns the name of the bucket that contains this repository. - GetBucket() string -} - -// IRepoService maintains a list of chart repositories that defines the scope of all -// repository based operations, such as search and chart reference resolution. -type IRepoService interface { - // ListRepos returns the list of all known chart repositories - ListRepos() (map[string]string, error) - // CreateRepo adds a known repository to the list - CreateRepo(repository IRepo) error - // GetRepoByURL returns the repository with the given name - GetRepoByURL(name string) (IRepo, error) - // GetRepoURLByName return url for a repository with the given name - GetRepoURLByName(name string) (string, error) - // GetRepoByChartURL returns the repository that backs the given URL - GetRepoByChartURL(URL string) (IRepo, error) - // DeleteRepo removes a known repository from the list - DeleteRepo(name string) error -} - -// IRepoProvider is a factory for IChartRepo instances. -type IRepoProvider interface { - GetChartByReference(reference string) (*chart.Chart, IChartRepo, error) - GetRepoByChartURL(URL string) (IChartRepo, error) - GetRepoByURL(URL string) (IChartRepo, error) -} - -// IGCSRepoProvider is a factory for GCS IRepo instances. -type IGCSRepoProvider interface { - GetGCSRepo(r IRepo) (IStorageRepo, error) -} diff --git a/pkg/util/httpclient.go b/pkg/util/httpclient.go deleted file mode 100644 index 43b3ff4de..000000000 --- a/pkg/util/httpclient.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "compress/gzip" - "fmt" - "io" - "io/ioutil" - "net/http" - "time" -) - -const ( - accHeader = "Accept-Encoding" - typeHeader = "Content-Type" - encHeader = "Content-Encoding" - gzipHeader = "gzip" -) - -// TODO (iantw): Consider creating the Duration objects up front... May just need an all around -// refactor if we want to support other types of backoff etc. -var sleepIntervals = []int{1, 1, 2, 3, 5, 8, 10} - -// Sleeper exposes a Sleep func which causes the current goroutine to sleep for the requested -// duration. -type Sleeper interface { - Sleep(d time.Duration) -} - -type sleeper struct { -} - -func (s sleeper) Sleep(d time.Duration) { - time.Sleep(d) -} - -// NewSleeper returns a new Sleeper. -func NewSleeper() Sleeper { - return sleeper{} -} - -// HTTPDoer is an interface for something that can 'Do' an http.Request and return an http.Response -// and error. -type HTTPDoer interface { - Do(req *http.Request) (resp *http.Response, err error) -} - -// HTTPClient is a higher level HTTP client which takes a URL and returns the response body as a -// string, along with the resulting status code and any errors. -type HTTPClient interface { - Get(url string) (body string, code int, err error) -} - -type httpClient struct { - retries uint - client HTTPDoer - sleep Sleeper -} - -// DefaultHTTPClient returns a default http client. -func DefaultHTTPClient() HTTPClient { - return NewHTTPClient(3, http.DefaultClient, NewSleeper()) -} - -// NewHTTPClient returns a new HTTPClient. -func NewHTTPClient(retries uint, c HTTPDoer, s Sleeper) HTTPClient { - ret := httpClient{} - ret.client = c - ret.sleep = s - ret.retries = retries - return ret -} - -func readBody(b io.ReadCloser, ctype string, encoding string) (body string, err error) { - defer b.Close() - var r io.Reader - if encoding == gzipHeader { - gr, err := gzip.NewReader(b) - if err != nil { - return "", err - } - r = gr - defer gr.Close() - } else if encoding == "" { - r = b - } else { - return "", fmt.Errorf("Unknown %s: %s", encHeader, encoding) - } - - // TODO(iantw): If we find a need, allow character set conversions... - // Unlikely to be an issue for now. - // if ctype != "" { - // r, err = charset.NewReader(r, ctype) - // - // if err != nil { - // return "", err - // } - // } - - bytes, err := ioutil.ReadAll(r) - return string(bytes), err -} - -// Get does an HTTP GET on the receiver. -func (client httpClient) Get(url string) (body string, code int, err error) { - retryCount := client.retries - numRetries := uint(0) - shouldRetry := true - for shouldRetry { - body = "" - code = 0 - err = nil - var req *http.Request - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return "", 0, err - } - - req.Header.Add(accHeader, gzipHeader) - - var r *http.Response - r, err = client.client.Do(req) - if err == nil { - body, err = readBody(r.Body, r.Header.Get(typeHeader), r.Header.Get(encHeader)) - if err == nil { - code = r.StatusCode - } - } - - if code != 200 { - if numRetries < retryCount { - numRetries = numRetries + 1 - sleepIndex := int(numRetries) - if numRetries >= uint(len(sleepIntervals)) { - sleepIndex = len(sleepIntervals) - 1 - } - d, _ := time.ParseDuration(string(sleepIntervals[sleepIndex]) + "s") - client.sleep.Sleep(d) - } else { - shouldRetry = false - } - } else { - shouldRetry = false - } - } - return -} diff --git a/pkg/util/httpclient_test.go b/pkg/util/httpclient_test.go deleted file mode 100644 index 001ebccc4..000000000 --- a/pkg/util/httpclient_test.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "bytes" - "compress/gzip" - "errors" - "io" - "net/http" - "strings" - "testing" - "time" -) - -type mockSleeper struct { - args []time.Duration -} - -func (m *mockSleeper) Sleep(d time.Duration) { - m.args = append(m.args, d) -} - -type responseAndError struct { - err error - resp *http.Response -} - -type testBody struct { - closed bool - body io.Reader -} - -func (tb *testBody) Read(p []byte) (n int, err error) { - return tb.body.Read(p) -} - -func (tb *testBody) Close() error { - tb.closed = true - return nil -} - -func createResponse(err error, code int, body string, shouldClose bool, - headers map[string]string) responseAndError { - httpBody := testBody{body: strings.NewReader(body), closed: !shouldClose} - header := http.Header{} - for k, v := range headers { - header.Add(k, v) - } - httpResponse := &http.Response{ - Body: &httpBody, - ContentLength: int64(len(body)), - StatusCode: code, - Header: header, - } - return responseAndError{err: err, resp: httpResponse} -} - -type mockDoer struct { - resp []responseAndError - t *testing.T - url string - headers map[string]string -} - -func (doer *mockDoer) Do(req *http.Request) (res *http.Response, err error) { - if req.URL.String() != doer.url { - doer.t.Errorf("Expected url %s but got url %s", doer.url, req.URL.String()) - } - - for k, v := range doer.headers { - if req.Header.Get(k) != v { - doer.t.Errorf("Expected header %s with value %s but found %s", k, v, req.Header.Get(k)) - } - } - - if len(doer.resp) == 0 { - doer.t.Errorf("Do method was called more times than expected.") - } - - res = doer.resp[0].resp - err = doer.resp[0].err - doer.resp = doer.resp[1:] - return -} - -func testClientDriver(md mockDoer, ms mockSleeper, expectedErr error, code int, - result string, t *testing.T) { - expectedCalls := len(md.resp) - client := NewHTTPClient(uint(expectedCalls)-1, &md, &ms) - - r, c, e := client.Get(md.url) - - if expectedCalls-1 != len(ms.args) { - t.Errorf("Expected %d calls to sleeper but found %d", expectedCalls-1, len(ms.args)) - } - - if r != result { - t.Errorf("Expected result %s but received %s", result, r) - } - - if c != code { - t.Errorf("Expected status code %d but received %d", code, c) - } - - if e != expectedErr { - t.Errorf("Expected error %s but received %s", expectedErr, e) - } -} - -func TestGzip(t *testing.T) { - doer := mockDoer{} - var b bytes.Buffer - gz := gzip.NewWriter(&b) - gz.Write([]byte("Test")) - gz.Flush() - gz.Close() - result := b.String() - - doer.resp = []responseAndError{ - createResponse(nil, 200, result, true, map[string]string{"Content-Encoding": "gzip"}), - } - - sleeper := mockSleeper{} - testClientDriver(doer, sleeper, nil, 200, "Test", t) -} - -func TestRetry(t *testing.T) { - doer := mockDoer{} - doer.resp = []responseAndError{ - createResponse(nil, 404, "", true, map[string]string{}), - createResponse(nil, 200, "Test", true, map[string]string{}), - } - - sleeper := mockSleeper{} - testClientDriver(doer, sleeper, nil, 200, "Test", t) -} - -func TestFail(t *testing.T) { - doer := mockDoer{} - err := errors.New("Error") - doer.resp = []responseAndError{ - createResponse(nil, 404, "", true, map[string]string{}), - createResponse(err, 0, "", false, map[string]string{}), - } - - sleeper := mockSleeper{} - testClientDriver(doer, sleeper, err, 0, "", t) -} diff --git a/pkg/util/httputil.go b/pkg/util/httputil.go deleted file mode 100644 index 8713e3abf..000000000 --- a/pkg/util/httputil.go +++ /dev/null @@ -1,251 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/http/httptest" - "net/url" - "strings" - - "github.com/ghodss/yaml" -) - -// ConvertURLsToStrings converts a slice of *url.URL to a slice of string. -func ConvertURLsToStrings(urls []*url.URL) []string { - var result []string - for _, u := range urls { - result = append(result, u.String()) - } - - return result -} - -// TrimURLScheme removes the scheme, if any, from an URL. -func TrimURLScheme(URL string) string { - parts := strings.SplitAfter(URL, "://") - if len(parts) > 1 { - return parts[1] - } - - return URL -} - -// A HandlerTester is a function that takes an HTTP method, an URL path, and a -// reader for a request body, creates a request from them, and serves it to the -// handler to which it was bound and returns a response recorder describing the -// outcome. -type HandlerTester func(method, path, ctype string, reader io.Reader) (*httptest.ResponseRecorder, error) - -// NewHandlerTester creates and returns a new HandlerTester for an http.Handler. -func NewHandlerTester(handler http.Handler) HandlerTester { - return func(method, path, ctype string, reader io.Reader) (*httptest.ResponseRecorder, error) { - r, err := http.NewRequest(method, path, reader) - if err != nil { - return nil, err - } - - r.Header.Set("Content-Type", ctype) - r.Header.Set("Accept", "*/*") - w := httptest.NewRecorder() - handler.ServeHTTP(w, r) - return w, nil - } -} - -// A ServerTester is a function that takes an HTTP method, an URL path, and a -// reader for a request body, creates a request from them, and serves it to a -// test server using the handler to which it was bound and returns the response. -type ServerTester func(method, path, ctype string, reader io.Reader) (*http.Response, error) - -// NewServerTester creates and returns a new NewServerTester for an http.Handler. -func NewServerTester(handler http.Handler) ServerTester { - return func(method, path, ctype string, reader io.Reader) (*http.Response, error) { - server := httptest.NewServer(handler) - defer server.Close() - request := fmt.Sprintf("%s/%s", server.URL, path) - r, err := http.NewRequest(method, request, reader) - if err != nil { - return nil, err - } - - r.Header.Set("Content-Type", ctype) - r.Header.Set("Accept", "*/*") - return http.DefaultClient.Do(r) - } -} - -const formContentType = "application/x-www-form-urlencoded; param=value" - -// TestWithURL invokes a HandlerTester with the given HTTP method, an URL path -// parsed from the given URL string, and a string reader on the query parameters -// parsed from the given URL string. -func (h HandlerTester) TestWithURL(method, urlString string) (*httptest.ResponseRecorder, error) { - request, err := url.Parse(urlString) - if err != nil { - return nil, err - } - - reader := strings.NewReader(request.Query().Encode()) - return h(method, request.Path, formContentType, reader) -} - -// TestHandlerWithURL creates a HandlerTester with the given handler, and tests -// it with the given HTTP method and URL string using HandlerTester.TestWithURL. -func TestHandlerWithURL(handler http.Handler, method, urlString string) (*httptest.ResponseRecorder, error) { - return NewHandlerTester(handler).TestWithURL(method, urlString) -} - -// LogHandlerText logs a line of text for a handler. -func LogHandlerText(handler string, v string) { - log.Printf("%s: %s\n", handler, v) -} - -// LogHandlerEntry logs the start of the given handler handling the given request. -func LogHandlerEntry(handler string, r *http.Request) { - log.Printf("%s: handling request:%s %s\n", handler, r.Method, r.URL.RequestURI()) -} - -// LogHandlerExit logs the response from the given handler with the given results. -func LogHandlerExit(handler string, statusCode int, status string, w http.ResponseWriter) { - log.Printf("%s: returning response: status code:%d, status:%s\n", handler, statusCode, status) -} - -// LogAndReturnError logs the given error and status to stderr, -// and then returns them as the HTTP response. -func LogAndReturnError(handler string, statusCode int, err error, w http.ResponseWriter) { - LogHandlerExit(handler, statusCode, err.Error(), w) - http.Error(w, err.Error(), statusCode) -} - -// LogHandlerExitWithText converts the given string to []byte, -// writes it to the response body, returns the given status, -// and then logs the response -func LogHandlerExitWithText(handler string, w http.ResponseWriter, v string, statusCode int) { - msg := []byte(v) - WriteResponse(handler, w, msg, "text/plain; charset=UTF-8", statusCode) - LogHandlerExit(handler, statusCode, string(msg), w) -} - -// LogHandlerExitWithJSON marshals the given object as JSON, -// writes it to the response body, returns the given status, and then logs the -// response. -func LogHandlerExitWithJSON(handler string, w http.ResponseWriter, v interface{}, statusCode int) { - j := MarshalAndWriteJSON(handler, w, v, statusCode) - LogHandlerExit(handler, statusCode, string(j), w) -} - -// MarshalAndWriteJSON marshals the given object as JSON, writes it -// to the response body, and then returns the given status. -func MarshalAndWriteJSON(handler string, w http.ResponseWriter, v interface{}, statusCode int) []byte { - j, err := json.Marshal(v) - if err != nil { - LogAndReturnError(handler, http.StatusInternalServerError, err, w) - return nil - } - - WriteJSON(handler, w, j, statusCode) - return j -} - -// WriteJSON writes the given bytes to the response body, sets the content type -// to "application/json; charset=UTF-8", and then returns the given status. -func WriteJSON(handler string, w http.ResponseWriter, j []byte, status int) { - WriteResponse(handler, w, j, "application/json; charset=UTF-8", status) -} - -// LogHandlerExitWithYAML marshals the given object as YAML, -// writes it to the response body, returns the given status, and then logs the -// response. -func LogHandlerExitWithYAML(handler string, w http.ResponseWriter, v interface{}, statusCode int) { - y := MarshalAndWriteYAML(handler, w, v, statusCode) - LogHandlerExit(handler, statusCode, string(y), w) -} - -// MarshalAndWriteYAML marshals the given object as YAML, writes it -// to the response body, and then returns the given status. -func MarshalAndWriteYAML(handler string, w http.ResponseWriter, v interface{}, statusCode int) []byte { - y, err := yaml.Marshal(v) - if err != nil { - LogAndReturnError(handler, http.StatusInternalServerError, err, w) - return nil - } - - WriteYAML(handler, w, y, statusCode) - return y -} - -// WriteYAML writes the given bytes to the response body, sets the content type -// to "application/x-yaml; charset=UTF-8", and then returns the given status. -func WriteYAML(handler string, w http.ResponseWriter, y []byte, status int) { - WriteResponse(handler, w, y, "application/x-yaml; charset=UTF-8", status) -} - -// WriteResponse writes the given bytes to the response body, sets the content -// type to the given value, and then returns the given status. -func WriteResponse(handler string, w http.ResponseWriter, v []byte, ct string, status int) { - // Header must be set before status is written - if len(v) > 0 { - w.Header().Set("Content-Type", ct) - } - - // Header and status must be written before content is written - w.WriteHeader(status) - if len(v) > 0 { - if _, err := w.Write(v); err != nil { - LogAndReturnError(handler, http.StatusInternalServerError, err, w) - } - } -} - -// ToYAMLOrError marshals the given object to YAML and returns either the -// resulting YAML or an error message. Useful when marshaling an object for -// a log entry. -func ToYAMLOrError(v interface{}) string { - y, err := yaml.Marshal(v) - if err != nil { - return fmt.Sprintf("yaml marshal failed:%s\n%v\n", err, v) - } - - return string(y) -} - -// ToJSONOrError marshals the given object to JSON and returns either the -// resulting YAML or an error message. Useful when marshaling an object for -// a log entry. -func ToJSONOrError(v interface{}) string { - j, err := json.Marshal(v) - if err != nil { - return fmt.Sprintf("json marshal failed:%s\n%v\n", err, v) - } - - return string(j) -} - -// IsHTTPURL returns true if a string is an HTTP URL. -func IsHTTPURL(s string) bool { - u, err := url.Parse(s) - if err != nil { - return false - } - - return u.Scheme == "http" || u.Scheme == "https" -} diff --git a/pkg/util/kubernetes.go b/pkg/util/kubernetes.go deleted file mode 100644 index a3adf62d5..000000000 --- a/pkg/util/kubernetes.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "fmt" - "log" - "net" - "net/url" - "os" - "strings" -) - -// KubernetesConfig defines the configuration options for talking to Kubernetes master -type KubernetesConfig struct { - KubePath string // The path to kubectl binary - KubeService string // DNS name of the kubernetes service - KubeServer string // The IP address and optional port of the kubernetes master - KubeInsecure bool // Do not check the server's certificate for validity - KubeConfig string // Path to a kubeconfig file - KubeCertAuth string // Path to a file for the certificate authority - KubeClientCert string // Path to a client certificate file - KubeClientKey string // Path to a client key file - KubeToken string // A service account token - KubeUsername string // The username to use for basic auth - KubePassword string // The password to use for basic auth -} - -// Kubernetes defines the interface for talking to Kubernetes. Currently the -// only implementation is through kubectl, but eventually this could be done -// via direct API calls. -type Kubernetes interface { - Get(name string, resourceType string) (string, error) - Create(resource string) (string, error) - Delete(resource string) (string, error) - Replace(resource string) (string, error) -} - -// KubernetesObject represents a native 'bare' Kubernetes object. -type KubernetesObject struct { - Kind string `json:"kind"` - APIVersion string `json:"apiVersion"` - Metadata map[string]interface{} `json:"metadata"` - Spec map[string]interface{} `json:"spec"` -} - -// KubernetesSecret represents a Kubernetes secret -type KubernetesSecret struct { - Kind string `json:"kind"` - APIVersion string `json:"apiVersion"` - Metadata map[string]string `json:"metadata"` - Data map[string]string `json:"data,omitempty"` -} - -// GetServiceURL takes a service name, a service port, and a default service URL, -// and returns a URL for accessing the service. It first looks for an environment -// variable set by Kubernetes by transposing the service name. If it can't find -// one, it looks up the service name in DNS. If that doesn't work, it returns the -// default service URL. If that's empty, it returns an HTTP localhost URL for the -// service port. If service port is empty, it panics. -func GetServiceURL(serviceName, servicePort, serviceURL string) (string, error) { - if serviceName != "" { - varBase := strings.Replace(serviceName, "-", "_", -1) - varName := strings.ToUpper(varBase) + "_PORT" - serviceURL := os.Getenv(varName) - if serviceURL != "" { - u, err := url.Parse(serviceURL) - if err != nil || u.Path != "" || u.Scheme != "tcp" { - return "", fmt.Errorf("malformed value: %s for envinronment variable: %s", serviceURL, varName) - } - - u.Scheme = "http" - return u.String(), nil - } - - if servicePort != "" { - addrs, err := net.LookupHost(serviceName) - if err == nil && len(addrs) > 0 { - return fmt.Sprintf("http://%s:%s", addrs[0], servicePort), nil - } - } - } - - if serviceURL != "" { - return serviceURL, nil - } - - if servicePort != "" { - serviceURL = fmt.Sprintf("http://localhost:%s", servicePort) - return serviceURL, nil - } - - err := fmt.Errorf("cannot resolve service:%v in environment:%v\n", serviceName, os.Environ()) - return "", err -} - -// GetServiceURLOrDie calls GetServiceURL and exits if it returns an error. -func GetServiceURLOrDie(serviceName, servicePort, serviceURL string) string { - URL, err := GetServiceURL(serviceName, servicePort, serviceURL) - if err != nil { - log.Fatal(err) - } - - return URL -} diff --git a/pkg/util/kubernetes_kubectl.go b/pkg/util/kubernetes_kubectl.go deleted file mode 100644 index ff889aae4..000000000 --- a/pkg/util/kubernetes_kubectl.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "bytes" - "fmt" - "log" - "net" - "os" - "os/exec" -) - -// KubernetesKubectl implements the interface for talking to Kubernetes by wrapping calls -// via kubectl. -type KubernetesKubectl struct { - KubePath string - // Base level arguments to kubectl. User commands/arguments get appended to this. - Arguments []string -} - -// NewKubernetesKubectl creates a new Kubernetes kubectl wrapper. -func NewKubernetesKubectl(config *KubernetesConfig) Kubernetes { - if config.KubePath == "" { - log.Fatalf("kubectl path cannot be empty") - } - // If a configuration file is specified, then it will provide the server - // address and credentials. If not, then we check for the server address - // and credentials as individual flags. - var args []string - if config.KubeConfig != "" { - config.KubeConfig = os.ExpandEnv(config.KubeConfig) - args = append(args, fmt.Sprintf("--kubeconfig=%s", config.KubeConfig)) - } else { - if config.KubeServer != "" { - args = append(args, fmt.Sprintf("--server=https://%s", config.KubeServer)) - } else if config.KubeService != "" { - addrs, err := net.LookupHost(config.KubeService) - if err != nil || len(addrs) < 1 { - log.Fatalf("cannot resolve DNS name: %v", config.KubeService) - } - - args = append(args, fmt.Sprintf("--server=https://%s", addrs[0])) - } - - if config.KubeInsecure { - args = append(args, fmt.Sprintf("--insecure-skip-tls-verify=%t", config.KubeInsecure)) - } else { - if config.KubeCertAuth != "" { - args = append(args, fmt.Sprintf("--certificate-authority=%s", config.KubeCertAuth)) - if config.KubeClientCert == "" { - args = append(args, fmt.Sprintf("--client-certificate=%s", config.KubeClientCert)) - } - - if config.KubeClientKey == "" { - args = append(args, fmt.Sprintf("--client-key=%s", config.KubeClientKey)) - } - } - } - if config.KubeToken != "" { - args = append(args, fmt.Sprintf("--token=%s", config.KubeToken)) - } else { - if config.KubeUsername != "" { - args = append(args, fmt.Sprintf("--username=%s", config.KubeUsername)) - } - - if config.KubePassword != "" { - args = append(args, fmt.Sprintf("--password=%s", config.KubePassword)) - } - } - } - return &KubernetesKubectl{config.KubePath, args} -} - -// Get runs a kubectl get command for a named resource of a given type. -func (k *KubernetesKubectl) Get(name string, resourceType string) (string, error) { - // Specify output as json rather than human readable for easier machine parsing - args := []string{"get", - "-o", - "json", - resourceType, - name} - return k.execute(args, "") -} - -// Create runs a kubectl create command for a given resource. -func (k *KubernetesKubectl) Create(resource string) (string, error) { - args := []string{"create"} - return k.execute(args, resource) -} - -// Delete runs a kubectl delete command for a given resource. -func (k *KubernetesKubectl) Delete(resource string) (string, error) { - args := []string{"delete"} - return k.execute(args, resource) -} - -// Replace runs a kubectl replace command for a given resource. -func (k *KubernetesKubectl) Replace(resource string) (string, error) { - args := []string{"replace"} - return k.execute(args, resource) -} - -func (k *KubernetesKubectl) execute(args []string, input string) (string, error) { - if len(input) > 0 { - args = append(args, "-f", "-") - } - - // Tack on the common arguments to the end of the command line - args = append(args, k.Arguments...) - cmd := exec.Command(k.KubePath, args...) - cmd.Stdin = bytes.NewBuffer([]byte(input)) - - // Combine stdout and stderr into a single dynamically resized buffer - combined := &bytes.Buffer{} - cmd.Stdout = combined - cmd.Stderr = combined - - if err := cmd.Start(); err != nil { - e := fmt.Errorf("cannot start kubectl %s %#v", combined.String(), err) - log.Printf("%s", e) - return combined.String(), e - } - - if err := cmd.Wait(); err != nil { - e := fmt.Errorf("kubectl failed %s", combined.String()) - log.Printf("%s", e) - return combined.String(), e - } - log.Printf("kubectl succeeded: SysTime: %v UserTime: %v", - cmd.ProcessState.SystemTime(), cmd.ProcessState.UserTime()) - return combined.String(), nil -} diff --git a/pkg/util/kubernetesutil.go b/pkg/util/kubernetesutil.go deleted file mode 100644 index a34fea60a..000000000 --- a/pkg/util/kubernetesutil.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "fmt" - "time" - - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/common" -) - -// ParseKubernetesObject parses a Kubernetes API object in YAML format. -func ParseKubernetesObject(object []byte) (*common.Resource, error) { - o := &KubernetesObject{} - if err := yaml.Unmarshal(object, &o); err != nil { - return nil, fmt.Errorf("cannot unmarshal native kubernetes object (%s): %s", object, err) - } - - // Ok, it appears to be a valid object, create a Resource out of it. - r := &common.Resource{} - md, ok := o.Metadata["name"].(string) - if !ok { - return nil, fmt.Errorf("cannot parse native kubernetes object (%s)", object) - } - - r.Name = getRandomName(md) - r.Type = o.Kind - - r.Properties = make(map[string]interface{}) - if err := yaml.Unmarshal(object, &r.Properties); err != nil { - return nil, fmt.Errorf("cannot unmarshal native kubernetes object (%s): %s", object, err) - } - - return r, nil -} - -func getRandomName(prefix string) string { - return fmt.Sprintf("%s-%d", prefix, time.Now().UTC().UnixNano()) -} diff --git a/pkg/util/kubernetesutil_test.go b/pkg/util/kubernetesutil_test.go deleted file mode 100644 index 87991f6c4..000000000 --- a/pkg/util/kubernetesutil_test.go +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "fmt" - "reflect" - "testing" - - "github.com/ghodss/yaml" - - "github.com/kubernetes/helm/pkg/common" -) - -var serviceInput = ` - kind: "Service" - apiVersion: "v1" - metadata: - name: "mock" - labels: - app: "mock" - spec: - ports: - - - protocol: "TCP" - port: 99 - targetPort: 9949 - selector: - app: "mock" -` - -var serviceExpected = ` -name: mock -type: Service -properties: - kind: "Service" - apiVersion: "v1" - metadata: - name: "mock" - labels: - app: "mock" - spec: - ports: - - - protocol: "TCP" - port: 99 - targetPort: 9949 - selector: - app: "mock" -` - -var rcInput = ` - kind: "ReplicationController" - apiVersion: "v1" - metadata: - name: "mockname" - labels: - app: "mockapp" - foo: "bar" - spec: - replicas: 1 - selector: - app: "mockapp" - template: - metadata: - labels: - app: "mocklabel" - spec: - containers: - - - name: "mock-container" - image: "kubernetes/pause" - ports: - - - containerPort: 9949 - protocol: "TCP" - - - containerPort: 9949 - protocol: "TCP" -` - -var rcExpected = ` -name: mockname -type: ReplicationController -properties: - kind: "ReplicationController" - apiVersion: "v1" - metadata: - name: "mockname" - labels: - app: "mockapp" - foo: "bar" - spec: - replicas: 1 - selector: - app: "mockapp" - template: - metadata: - labels: - app: "mocklabel" - spec: - containers: - - - name: "mock-container" - image: "kubernetes/pause" - ports: - - - containerPort: 9949 - protocol: "TCP" - - - containerPort: 9949 - protocol: "TCP" -` - -func unmarshalResource(t *testing.T, object []byte) (*common.Resource, error) { - r := &common.Resource{} - if err := yaml.Unmarshal([]byte(object), &r); err != nil { - t.Errorf("cannot unmarshal test object (%#v)", err) - return nil, err - } - return r, nil -} - -func testConversion(t *testing.T, object []byte, expected []byte) { - e, err := unmarshalResource(t, expected) - if err != nil { - t.Fatalf("Failed to unmarshal expected Resource: %v", err) - } - - result, err := ParseKubernetesObject(object) - if err != nil { - t.Fatalf("ParseKubernetesObject failed: %v", err) - } - // Since the object name gets created on the fly, we have to rejigger the returned object - // slightly to make sure the DeepEqual works as expected. - // First validate the name matches the expected format. - var i int - format := e.Name + "-%d" - count, err := fmt.Sscanf(result.Name, format, &i) - if err != nil || count != 1 { - t.Errorf("Name is not as expected, wanted of the form %s got %s", format, result.Name) - } - e.Name = result.Name - if !reflect.DeepEqual(result, e) { - t.Errorf("expected %+v but found %+v", e, result) - } - -} - -func TestSimple(t *testing.T) { - testConversion(t, []byte(rcInput), []byte(rcExpected)) - testConversion(t, []byte(serviceInput), []byte(serviceExpected)) -} diff --git a/pkg/util/templateutil.go b/pkg/util/templateutil.go deleted file mode 100644 index ea111448c..000000000 --- a/pkg/util/templateutil.go +++ /dev/null @@ -1,196 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "archive/tar" - "fmt" - "io" - "io/ioutil" - "log" - "path" - "path/filepath" - - "github.com/ghodss/yaml" - "github.com/kubernetes/helm/pkg/common" -) - -// NewTemplateFromType creates and returns a new template whose content -// is a YAML marshaled resource assembled from the supplied arguments. -func NewTemplateFromType(name, typeName string, properties map[string]interface{}) (*common.Template, error) { - resource := &common.Resource{ - Name: name, - Type: typeName, - Properties: properties, - } - - config := common.Configuration{Resources: []*common.Resource{resource}} - content, err := yaml.Marshal(config) - if err != nil { - return nil, fmt.Errorf("error: %s\ncannot marshal configuration: %v\n", err, config) - } - - template := &common.Template{ - Name: name, - Content: string(content), - Imports: []*common.ImportFile{}, - } - - return template, nil -} - -// NewTemplateFromArchive creates and returns a new template whose content -// and imported files are read from the supplied archive. -func NewTemplateFromArchive(name string, r io.Reader, importFileNames []string) (*common.Template, error) { - var content []byte - imports, err := collectImportFiles(importFileNames) - if err != nil { - return nil, err - } - - tr := tar.NewReader(r) - for i := 0; true; i++ { - hdr, err := tr.Next() - if err == io.EOF { - break - } - - if err != nil { - return nil, err - } - - if hdr.Name != name { - importFileData, err := ioutil.ReadAll(tr) - if err != nil { - return nil, fmt.Errorf("cannot read archive file %s: %s", hdr.Name, err) - } - - imports = append(imports, - &common.ImportFile{ - Name: path.Base(hdr.Name), - Content: string(importFileData), - }) - } else { - content, err = ioutil.ReadAll(tr) - if err != nil { - return nil, fmt.Errorf("cannot read %s from archive: %s", name, err) - } - } - } - - if len(content) < 1 { - return nil, fmt.Errorf("cannot find %s in archive", name) - } - - return &common.Template{ - Name: name, - Content: string(content), - Imports: imports, - }, nil -} - -// NewTemplateFromReader creates and returns a new template whose content -// is read from the supplied reader. -func NewTemplateFromReader(name string, r io.Reader, importFileNames []string) (*common.Template, error) { - content, err := ioutil.ReadAll(r) - if err != nil { - return nil, fmt.Errorf("cannot read archive %s: %s", name, err) - } - - return newTemplateFromContentAndImports(name, string(content), importFileNames) -} - -// NewTemplateFromRootTemplate creates and returns a new template whose content -// and imported files are constructed from reading the root template, parsing out -// the imports section and reading the imports from there -func NewTemplateFromRootTemplate(templateFileName string) (*common.Template, error) { - templateDir := filepath.Dir(templateFileName) - content, err := ioutil.ReadFile(templateFileName) - if err != nil { - return nil, fmt.Errorf("cannot read template file (%s): %s", err, templateFileName) - } - - var c map[string]interface{} - err = yaml.Unmarshal([]byte(content), &c) - if err != nil { - log.Fatalf("Cannot parse template: %v", err) - } - - // For each of the imports, grab the import file - var imports []string - if c["imports"] != nil { - for _, importFile := range c["imports"].([]interface{}) { - var fileName = importFile.(map[string]interface{})["path"].(string) - imports = append(imports, templateDir+"/"+fileName) - } - } - - return NewTemplateFromFileNames(templateFileName, imports[0:]) -} - -// NewTemplateFromFileNames creates and returns a new template whose content -// and imported files are read from the supplied file names. -func NewTemplateFromFileNames( - templateFileName string, - importFileNames []string, -) (*common.Template, error) { - content, err := ioutil.ReadFile(templateFileName) - if err != nil { - return nil, fmt.Errorf("cannot read template file %s: %s", templateFileName, err) - } - - name := path.Base(templateFileName) - return newTemplateFromContentAndImports(name, string(content), importFileNames) -} - -func newTemplateFromContentAndImports( - name, content string, - importFileNames []string, -) (*common.Template, error) { - if len(content) < 1 { - return nil, fmt.Errorf("supplied configuration is empty") - } - - imports, err := collectImportFiles(importFileNames) - if err != nil { - return nil, err - } - - return &common.Template{ - Name: name, - Content: content, - Imports: imports, - }, nil -} - -func collectImportFiles(importFileNames []string) ([]*common.ImportFile, error) { - imports := []*common.ImportFile{} - for _, importFileName := range importFileNames { - importFileData, err := ioutil.ReadFile(importFileName) - if err != nil { - return nil, fmt.Errorf("cannot read import file %s: %s", importFileName, err) - } - - imports = append(imports, - &common.ImportFile{ - Name: path.Base(importFileName), - Content: string(importFileData), - }) - } - - return imports, nil -} diff --git a/pkg/util/templateutil_test.go b/pkg/util/templateutil_test.go deleted file mode 100644 index e6e66ac9f..000000000 --- a/pkg/util/templateutil_test.go +++ /dev/null @@ -1,148 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -package util - -import ( - "archive/tar" - "bytes" - "testing" - - "github.com/ghodss/yaml" -) - -const invalidFileName = "afilethatdoesnotexist" - -var importFileNames = []string{ - "../test/replicatedservice.py", -} - -var ( - testTemplateName = "expandybird" - testTemplateType = "replicatedservice.py" - testTemplateProperties = ` -service_port: 8080 -target_port: 8080 -container_port: 8080 -external_service: true -replicas: 3 -image: gcr.io/kubernetes-helm/expandybird -labels: - app: expandybird -` -) - -func TestNewTemplateFromType(t *testing.T) { - var properties map[string]interface{} - if err := yaml.Unmarshal([]byte(testTemplateProperties), &properties); err != nil { - t.Fatalf("cannot unmarshal test data: %s", err) - } - - _, err := NewTemplateFromType(testTemplateName, testTemplateType, properties) - if err != nil { - t.Fatalf("cannot create template from type %s: %s", testTemplateType, err) - } -} - -func TestNewTemplateFromReader(t *testing.T) { - r := bytes.NewReader([]byte{}) - if _, err := NewTemplateFromReader("test", r, nil); err == nil { - t.Fatalf("expected error did not occur for empty input: %s", err) - } - - r = bytes.NewReader([]byte("test")) - if _, err := NewTemplateFromReader("test", r, nil); err != nil { - t.Fatalf("cannot read test template: %s", err) - } -} - -type archiveBuilder []struct { - Name, Body string -} - -var invalidFiles = archiveBuilder{ - {"testFile1.yaml", ""}, -} - -var validFiles = archiveBuilder{ - {"testFile1.yaml", "testFile:1"}, - {"testFile2.yaml", "testFile:2"}, -} - -func generateArchive(t *testing.T, files archiveBuilder) *bytes.Reader { - buffer := new(bytes.Buffer) - tw := tar.NewWriter(buffer) - for _, file := range files { - hdr := &tar.Header{ - Name: file.Name, - Mode: 0600, - Size: int64(len(file.Body)), - } - - if err := tw.WriteHeader(hdr); err != nil { - t.Fatal(err) - } - - if _, err := tw.Write([]byte(file.Body)); err != nil { - t.Fatal(err) - } - } - - if err := tw.Close(); err != nil { - t.Fatal(err) - } - - r := bytes.NewReader(buffer.Bytes()) - return r -} - -func TestNewTemplateFromArchive(t *testing.T) { - r := bytes.NewReader([]byte{}) - if _, err := NewTemplateFromArchive("", r, nil); err == nil { - t.Fatalf("expected error did not occur for empty input: %s", err) - } - - r = bytes.NewReader([]byte("test")) - if _, err := NewTemplateFromArchive("", r, nil); err == nil { - t.Fatalf("expected error did not occur for non archive file:%s", err) - } - - r = generateArchive(t, invalidFiles) - if _, err := NewTemplateFromArchive(invalidFiles[0].Name, r, nil); err == nil { - t.Fatalf("expected error did not occur for empty file in archive") - } - - r = generateArchive(t, validFiles) - if _, err := NewTemplateFromArchive("", r, nil); err == nil { - t.Fatalf("expected error did not occur for missing file in archive") - } - - r = generateArchive(t, validFiles) - if _, err := NewTemplateFromArchive(validFiles[1].Name, r, nil); err != nil { - t.Fatalf("cannnot create template from valid archive") - } -} - -func TestNewTemplateFromFileNames(t *testing.T) { - if _, err := NewTemplateFromFileNames(invalidFileName, importFileNames); err == nil { - t.Fatalf("expected error did not occur for invalid template file name") - } - - _, err := NewTemplateFromFileNames(invalidFileName, []string{"afilethatdoesnotexist"}) - if err == nil { - t.Fatalf("expected error did not occur for invalid import file names") - } -} diff --git a/pkg/version/version.go b/pkg/version/version.go deleted file mode 100644 index 91711dae5..000000000 --- a/pkg/version/version.go +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -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. -*/ - -// Package version represents the current version of the project. -package version - -// Version is the current version of the Helm. -// Update this whenever making a new release. -// The version is of the format Major.Minor.Patch -// Increment major number for new feature additions and behavioral changes. -// Increment minor number for bug fixes and performance enhancements. -// Increment patch number for critical fixes to existing releases. -var Version = "0.0.1" diff --git a/rootfs/Makefile b/rootfs/Makefile deleted file mode 100644 index b584a53d9..000000000 --- a/rootfs/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -SUBDIRS := expandybird/. resourcifier/. manager/. -TARGETS := info push container clean - -SUBDIRS_TARGETS := \ - $(foreach t,$(TARGETS),$(addsuffix $t,$(SUBDIRS))) - -.PHONY : $(TARGETS) $(SUBDIRS_TARGETS) - -$(TARGETS) : % : $(addsuffix %,$(SUBDIRS)) - -$(SUBDIRS_TARGETS) : - $(MAKE) -C $(@D) $(@F:.%=%) diff --git a/rootfs/README.md b/rootfs/README.md deleted file mode 100644 index 161bc9e9f..000000000 --- a/rootfs/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# RootFS - -This directory stores all files that should be copied to the rootfs of a -Docker container. The files should be stored according to the correct -directory structure of the destination container. For example: - -``` -rootfs/bin -> /bin -rootfs/usr/local/share -> /usr/local/share -``` - -## Dockerfile - -A Dockerfile in the rootfs is used to build the image. Where possible, -compilation should not be done in this Dockerfile, since we are -interested in deploying the smallest possible images. - -Example: - -```Dockerfile -FROM alpine:3.2 - -COPY . / - -ENTRYPOINT ["/usr/local/bin/boot"] -``` diff --git a/rootfs/expandybird/.dockerignore b/rootfs/expandybird/.dockerignore deleted file mode 100644 index f6fb65317..000000000 --- a/rootfs/expandybird/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -.dockerignore -Dockerfile -Makefile diff --git a/rootfs/expandybird/Dockerfile b/rootfs/expandybird/Dockerfile deleted file mode 100644 index 559041c4a..000000000 --- a/rootfs/expandybird/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -FROM alpine:3.3 - -COPY . / - -# install common packages -RUN apk add --no-cache curl python py-pip - -# install dependencies -RUN pip install --disable-pip-version-check --no-cache-dir -r /opt/expansion/requirements.txt - -# define execution environment -CMD ["/bin/expandybird", "-expansion_binary", "/opt/expansion/expansion.py"] -EXPOSE 8000 diff --git a/rootfs/expandybird/Makefile b/rootfs/expandybird/Makefile deleted file mode 100644 index 80c03a30c..000000000 --- a/rootfs/expandybird/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -IMAGE ?= expandybird - -include ../include.mk - -.PHONY: extras -extras: expansion - -.PHONY: expansion -expansion: - mkdir -p ./opt && cp -R ../../expansion ./opt diff --git a/rootfs/include.mk b/rootfs/include.mk deleted file mode 100644 index f52dcfe2c..000000000 --- a/rootfs/include.mk +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 The Kubernetes Authors All rights reserved. -# -# 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. - -# If you update this image please check the tag value before pushing. - -DEFAULT_REGISTRY := gcr.io -DOCKER_REGISTRY ?= $(DEFAULT_REGISTRY) - -# Support both local and remote repos, and support no project. -ifeq ($(DOCKER_PROJECT),) -PREFIX := $(DOCKER_REGISTRY) -else -PREFIX := $(DOCKER_REGISTRY)/$(DOCKER_PROJECT) -endif - -FULL_IMAGE := $(PREFIX)/$(IMAGE) - -TAG ?= git-$(shell git rev-parse --short HEAD) - -DEFAULT_PLATFORM := linux -PLATFORM ?= $(DEFAULT_PLATFORM) - -DEFAULT_ARCH := amd64 -ARCH ?= $(DEFAULT_ARCH) - - -.PHONY: clean -clean: - rm -rf bin opt - -.PHONY: info -info: - @echo "Build tag: ${TAG}" - @echo "Registry: ${DOCKER_REGISTRY}" - @echo "Project: ${DOCKER_PROJECT}" - @echo "Image: ${IMAGE}" - @echo "Platform: ${PLATFORM}" - @echo "Arch: ${ARCH}" - -.PHONY : .project - -.PHONY: push -push: container -ifeq ($(DOCKER_REGISTRY),gcr.io) - gcloud docker push $(FULL_IMAGE):$(TAG) - gcloud docker push $(FULL_IMAGE):latest -else - docker push $(FULL_IMAGE):$(TAG) - docker push $(FULL_IMAGE):latest -endif - -.PHONY: container -container: .project .docker binary extras - docker build -t $(FULL_IMAGE):$(TAG) -f Dockerfile . - docker tag -f $(FULL_IMAGE):$(TAG) $(FULL_IMAGE):latest - -.project: -ifeq ($(DOCKER_REGISTRY), gcr.io) -ifeq ($(DOCKER_PROJECT),) - $(error "Both DOCKER_REGISTRY and DOCKER_PROJECT must be set.") -endif -endif - -.docker: - @if [[ -z `which docker` ]] || ! docker --version &> /dev/null; then echo "docker is not installed correctly"; exit 1; fi - -.PHONY: binary -binary: - @if [[ ! -x "bin/$(IMAGE)" ]] ; then echo "binary bin/$(IMAGE) not found" ; exit 1 ; fi - -.PHONY: kubectl -kubectl: -ifeq ("$(wildcard bin/$(KUBE_VERSION))", "") - touch bin/$(KUBE_VERSION) - curl -fsSL -o bin/kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBE_VERSION}/bin/linux/386/kubectl - chmod +x bin/kubectl -endif diff --git a/rootfs/manager/.dockerignore b/rootfs/manager/.dockerignore deleted file mode 100644 index f6fb65317..000000000 --- a/rootfs/manager/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -.dockerignore -Dockerfile -Makefile diff --git a/rootfs/manager/Dockerfile b/rootfs/manager/Dockerfile deleted file mode 100644 index 189ae1a4a..000000000 --- a/rootfs/manager/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -FROM alpine:3.3 -COPY . / - -# Install CA certs -RUN apk add --no-cache ca-certificates - -EXPOSE 8080 -CMD ["/bin/manager", "--kubectl=/bin/kubectl"] diff --git a/rootfs/manager/Makefile b/rootfs/manager/Makefile deleted file mode 100644 index 0b1f4c681..000000000 --- a/rootfs/manager/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -IMAGE ?= manager -KUBE_VERSION ?= v1.1.7 - -include ../include.mk - -.PHONY: extras -extras: kubectl diff --git a/rootfs/resourcifier/.dockerignore b/rootfs/resourcifier/.dockerignore deleted file mode 100644 index f6fb65317..000000000 --- a/rootfs/resourcifier/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -.dockerignore -Dockerfile -Makefile diff --git a/rootfs/resourcifier/Dockerfile b/rootfs/resourcifier/Dockerfile deleted file mode 100644 index 8359b8a69..000000000 --- a/rootfs/resourcifier/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -FROM alpine:3.3 -COPY . / - -# Install CA certs -RUN apk add --no-cache ca-certificates - -EXPOSE 8080 -CMD ["/bin/resourcifier", "--kubectl=/bin/kubectl"] diff --git a/rootfs/resourcifier/Makefile b/rootfs/resourcifier/Makefile deleted file mode 100644 index bbf66ee66..000000000 --- a/rootfs/resourcifier/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -IMAGE ?= resourcifier -KUBE_VERSION ?= v1.1.7 - -include ../include.mk - -.PHONY: extras -extras: kubectl diff --git a/scripts/build-go.sh b/scripts/build-go.sh deleted file mode 100755 index 644762b02..000000000 --- a/scripts/build-go.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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 errexit -set -o pipefail - -[[ "$TRACE" ]] && set -x - -readonly REPO=github.com/kubernetes/helm -readonly DIR="${GOPATH}/src/${REPO}" - -source "${DIR}/scripts/common.sh" - -build_binaries "$@" - -exit 0 diff --git a/scripts/cluster/kube-system.yaml b/scripts/cluster/kube-system.yaml deleted file mode 100644 index 986f4b482..000000000 --- a/scripts/cluster/kube-system.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: kube-system diff --git a/scripts/cluster/skydns.yaml b/scripts/cluster/skydns.yaml deleted file mode 100644 index 720877d5d..000000000 --- a/scripts/cluster/skydns.yaml +++ /dev/null @@ -1,137 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: kube-dns-v10 - namespace: kube-system - labels: - k8s-app: kube-dns - version: v10 - kubernetes.io/cluster-service: "true" -spec: - replicas: 1 - selector: - k8s-app: kube-dns - version: v10 - template: - metadata: - labels: - k8s-app: kube-dns - version: v10 - kubernetes.io/cluster-service: "true" - spec: - containers: - - name: etcd - image: gcr.io/google_containers/etcd-amd64:2.2.1 - resources: - # keep request = limit to keep this container in guaranteed class - limits: - cpu: 100m - memory: 50Mi - requests: - cpu: 100m - memory: 50Mi - command: - - /usr/local/bin/etcd - - -data-dir - - /var/etcd/data - - -listen-client-urls - - http://127.0.0.1:2379,http://127.0.0.1:4001 - - -advertise-client-urls - - http://127.0.0.1:2379,http://127.0.0.1:4001 - - -initial-cluster-token - - skydns-etcd - volumeMounts: - - name: etcd-storage - mountPath: /var/etcd/data - - name: kube2sky - image: gcr.io/google_containers/kube2sky:1.12 - resources: - # keep request = limit to keep this container in guaranteed class - limits: - cpu: 100m - memory: 50Mi - requests: - cpu: 100m - memory: 50Mi - args: - # command = "/kube2sky" - - --domain=cluster.local - - name: skydns - image: gcr.io/google_containers/skydns:2015-10-13-8c72f8c - resources: - # keep request = limit to keep this container in guaranteed class - limits: - cpu: 100m - memory: 50Mi - requests: - cpu: 100m - memory: 50Mi - args: - # command = "/skydns" - - -machines=http://127.0.0.1:4001 - - -addr=0.0.0.0:53 - - -ns-rotate=false - - -domain=cluster.local. - ports: - - containerPort: 53 - name: dns - protocol: UDP - - containerPort: 53 - name: dns-tcp - protocol: TCP - livenessProbe: - httpGet: - path: /healthz - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 5 - readinessProbe: - httpGet: - path: /healthz - port: 8080 - scheme: HTTP - initialDelaySeconds: 1 - timeoutSeconds: 5 - - name: healthz - image: gcr.io/google_containers/exechealthz:1.0 - resources: - # keep request = limit to keep this container in guaranteed class - limits: - cpu: 10m - memory: 20Mi - requests: - cpu: 10m - memory: 20Mi - args: - - -cmd=nslookup kubernetes.default.svc.cluster.local 127.0.0.1 >/dev/null - - -port=8080 - ports: - - containerPort: 8080 - protocol: TCP - volumes: - - name: etcd-storage - emptyDir: {} - dnsPolicy: Default # Don't use cluster DNS. ---- -apiVersion: v1 -kind: Service -metadata: - name: kube-dns - namespace: kube-system - labels: - k8s-app: kube-dns - kubernetes.io/cluster-service: "true" - kubernetes.io/name: "KubeDNS" -spec: - selector: - k8s-app: kube-dns - clusterIP: 10.0.0.10 - ports: - - name: dns - port: 53 - protocol: UDP - - name: dns-tcp - port: 53 - protocol: TCP - diff --git a/scripts/common.sh b/scripts/common.sh deleted file mode 100644 index f596fb401..000000000 --- a/scripts/common.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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 errexit -set -o pipefail - -readonly ROOTFS="${DIR}/rootfs" - -readonly STATIC_TARGETS=(cmd/expandybird cmd/goexpander cmd/manager cmd/resourcifier) -readonly ALL_TARGETS=(${STATIC_TARGETS[@]} cmd/helm) - -error_exit() { - # Display error message and exit - echo "error: ${1:-"unknown error"}" 1>&2 - exit 1 -} - -is_osx() { - [[ "$(uname)" == "Darwin" ]] -} - -assign_version() { - if [[ -z "${VERSION:-}" ]]; then - VERSION=$(version_from_git) - fi -} - -assign_ldflags() { - if [[ -z "${LDFLAGS:-}" ]]; then - LDFLAGS="-s -X ${REPO}/pkg/version.Version=${VERSION}" - fi -} - -version_from_git() { - local git_tag=$(git describe --tags --abbrev=0 2>/dev/null) - local git_commit=$(git rev-parse --short HEAD) - echo "${git_tag}+${git_commit}" -} - -build_binary_static() { - local target="$1" - local basename="${target##*/}" - local context="${ROOTFS}/${basename}" - - echo "Building ${target}" - CGO_ENABLED=0 \ - GOOS=linux \ - GOARCH=amd64 \ - go build \ - -ldflags="${LDFLAGS}" \ - -a -installsuffix cgo \ - -o "${context}/bin/${basename}" \ - "${REPO}/${target}" -} - -build_binary_cross() { - local target="$1" - - echo "Building ${target}" - gox -verbose \ - -ldflags="${LDFLAGS}" \ - -os="linux darwin freebsd windows" \ - -arch="amd64 386 arm" \ - -output="bin/{{.OS}}-{{.Arch}}/{{.Dir}}" \ - "${REPO}/${target}" -} - -#TODO: accept specific os/arch -build_binaries() { - local -a targets=($@) - local build_type="${BUILD_TYPE:-}" - - if [[ ${#targets[@]} -eq 0 ]]; then - if [[ ${build_type} == STATIC ]]; then - targets=("${STATIC_TARGETS[@]}") - else - targets=("${ALL_TARGETS[@]}") - fi - fi - - assign_version - assign_ldflags - - for t in "${targets[@]}"; do - if [[ ${build_type} == STATIC ]]; then - build_binary_static "$t" - elif [[ ${build_type} == CROSS ]]; then - build_binary_cross "$t" - else - build_binary "$t" - fi - done -} - -build_binary() { - local target="$1" - local binary="${target##*/}" - local outfile="bin/${binary}" - - echo "Building ${target}" - go build -o "$outfile" -ldflags "$LDFLAGS" "${REPO}/${target}" -} diff --git a/scripts/coverage.sh b/scripts/coverage.sh deleted file mode 100755 index 7056ab43e..000000000 --- a/scripts/coverage.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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 -eo pipefail - -[[ "$TRACE" ]] && set -x - -COVERDIR=${COVERDIR:-.coverage} -COVERMODE=${COVERMODE:-atomic} -PACKAGES=($(go list $(glide novendor))) - -if [[ ! -d "$COVERDIR" ]]; then - mkdir -p "$COVERDIR" -fi - -echo "mode: ${COVERMODE}" > "${COVERDIR}/coverage.out" - -for d in "${PACKAGES[@]}"; do - go test -coverprofile=profile.out -covermode="$COVERMODE" "$d" - if [ -f profile.out ]; then - sed "/mode: $COVERMODE/d" profile.out >> "${COVERDIR}/coverage.out" - rm profile.out - fi -done - -go tool cover -html "${COVERDIR}/coverage.out" -o "${COVERDIR}/coverage.html" diff --git a/scripts/docker.sh b/scripts/docker.sh deleted file mode 100644 index c6174598a..000000000 --- a/scripts/docker.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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 -eo pipefail - -docker_detect_host_ip() { - if [ -n "$DOCKER_HOST" ]; then - awk -F'[/:]' '{print $4}' <<< "$DOCKER_HOST" - else - ifconfig docker0 \ - | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' \ - | grep -Eo '([0-9]*\.){3}[0-9]*' >/dev/null 2>&1 || : - fi -} - -DOCKER_HOST_IP=$(docker_detect_host_ip) - -is_docker_machine() { - [[ $(docker-machine active 2>/dev/null) ]] -} - -active_docker_machine() { - if command -v docker-machine >/dev/null 2>&1; then - docker-machine active - fi -} - -delete_container() { - local container=("$@") - - docker stop "${container[@]}" &>/dev/null || : - docker wait "${container[@]}" &>/dev/null || : - docker rm --force --volumes "${container[@]}" &>/dev/null || : -} - -dev_registry() { - if docker inspect registry >/dev/null 2>&1; then - docker start registry - else - docker run --restart="always" -d -p 5000:5000 --name registry registry:2 - fi -} diff --git a/scripts/kube-down.sh b/scripts/kube-down.sh deleted file mode 100755 index 749e164f9..000000000 --- a/scripts/kube-down.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -# Tear down kubernetes in docker - -set -eo pipefail -[[ "$TRACE" ]] && set -x - -HELM_ROOT="${BASH_SOURCE[0]%/*}/.." -source "${HELM_ROOT}/scripts/common.sh" -source "${HELM_ROOT}/scripts/docker.sh" - -KUBE_PORT=${KUBE_PORT:-8080} -KUBE_MASTER_IP=${KUBE_MASTER_IP:-$DOCKER_HOST_IP} -KUBE_MASTER_IP=${KUBE_MASTER_IP:-localhost} -KUBECTL="kubectl -s ${KUBE_MASTER_IP}:${KUBE_PORT}" - -delete_kube_resources() { - echo "Deleting resources in kubernetes..." - - $KUBECTL delete replicationcontrollers,services,pods,secrets --all > /dev/null 2>&1 || : - $KUBECTL delete replicationcontrollers,services,pods,secrets --all --namespace=kube-system > /dev/null 2>&1 || : - $KUBECTL delete namespace kube-system > /dev/null 2>&1 || : -} - -delete_hyperkube_containers() { - echo "Stopping kubelet..." - delete_container kubelet - - echo "Stopping remaining kubernetes containers..." - local kube_containers=($(docker ps -aqf "name=k8s_")) - if [[ "${#kube_containers[@]}" -gt 0 ]]; then - delete_container "${kube_containers[@]}" - fi - - echo "Stopping etcd..." - delete_container etcd -} - -detect_master() { - local cc=$(kubectl config view -o jsonpath="{.current-context}") - local cluster=$(kubectl config view -o jsonpath="{.contexts[?(@.name == \"${cc}\")].context.cluster}") - kubectl config view -o jsonpath="{.clusters[?(@.name == \"${cluster}\")].cluster.server}" -} - -main() { - if [ "$1" != "--force" ]; then - echo "WARNING: You are about to destroy kubernetes on $(detect_master)" - read -p "Press [Enter] key to continue..." - fi - - echo "Bringing down the kube..." - - delete_kube_resources - delete_hyperkube_containers - - echo "done." -} - -main "$@" - -exit 0 diff --git a/scripts/kube-up.sh b/scripts/kube-up.sh deleted file mode 100755 index 23bd0d4c3..000000000 --- a/scripts/kube-up.sh +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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. - -# Start a kubenetes cluster in docker -# -# Tested on darwin using docker-machine and linux - -set -eo pipefail -[[ "$TRACE" ]] && set -x - -HELM_ROOT="${BASH_SOURCE[0]%/*}/.." -source "${HELM_ROOT}/scripts/common.sh" -source "${HELM_ROOT}/scripts/docker.sh" - -K8S_VERSION=${K8S_VERSION:-1.2.1} -KUBE_PORT=${KUBE_PORT:-8080} -KUBE_MASTER_IP=${KUBE_MASTER_IP:-$DOCKER_HOST_IP} -KUBE_MASTER_IP=${KUBE_MASTER_IP:-localhost} -KUBE_CONTEXT=${KUBE_CONTEXT:-docker} - -KUBECTL="kubectl -s ${KUBE_MASTER_IP}:${KUBE_PORT}" - -require_command() { - if ! command -v "$1" >/dev/null 2>&1; then - error_exit "Cannot find command ${1}" - fi -} - -verify_prereqs() { - echo "Verifying Prerequisites...." - - require_command docker - require_command kubectl - - if is_osx; then - require_command docker-machine - fi - - if ! docker info > /dev/null 2>&1 ; then - error_exit "Can't connect to 'docker' daemon. please fix and retry." - fi - - if [[ ! $(docker version --format {{.Server.Version}}) == "1.10.3" ]]; then - error_exit "docker version 1.10.3 is required" - fi - - echo "You are golden, carry on..." -} - -setup_iptables() { - local machine=$(active_docker_machine) - if [ -z "$machine" ]; then - return - fi - - echo "Adding iptables hackery for docker-machine..." - - local machine_ip=$(docker-machine ip "$machine") - local iptables_rule="PREROUTING -p tcp -d ${machine_ip} --dport ${KUBE_PORT} -j DNAT --to-destination 127.0.0.1:${KUBE_PORT}" - - if ! docker-machine ssh "${machine}" "sudo /usr/local/sbin/iptables -t nat -C ${iptables_rule}" &> /dev/null; then - docker-machine ssh "${machine}" "sudo /usr/local/sbin/iptables -t nat -I ${iptables_rule}" - fi -} - -start_kubernetes() { - echo "Getting the party going..." - - echo "Starting etcd" - docker run \ - --name=etcd \ - --net=host \ - -d \ - gcr.io/google_containers/etcd:2.2.1 \ - /usr/local/bin/etcd \ - --listen-client-urls=http://127.0.0.1:4001 \ - --advertise-client-urls=http://127.0.0.1:4001 >/dev/null - - echo "Starting kubelet" - docker run \ - --name=kubelet \ - --volume=/:/rootfs:ro \ - --volume=/sys:/sys:ro \ - --volume=/var/lib/docker/:/var/lib/docker:rw \ - --volume=/var/run:/var/run:rw \ - --volume=/var/lib/kubelet:/var/lib/kubelet:shared \ - --net=host \ - --pid=host \ - --privileged=true \ - -d \ - gcr.io/google_containers/hyperkube-amd64:v${K8S_VERSION} \ - /hyperkube kubelet \ - --hostname-override="127.0.0.1" \ - --address="0.0.0.0" \ - --api-servers=http://localhost:${KUBE_PORT} \ - --config=/etc/kubernetes/manifests-multi \ - --cluster-dns=10.0.0.10 \ - --cluster-domain=cluster.local \ - --allow-privileged=true --v=2 >/dev/null -} - -wait_for_kubernetes_cluster() { - echo "Waiting for Kubernetes cluster to become available..." - while true; do - local running_count=$($KUBECTL get pods --all-namespaces --no-headers 2>/dev/null | grep "Running" | wc -l) - # We expect to have 3 running pods - master, kube-proxy, and dns - if [ "$running_count" -ge 3 ]; then - break - fi - sleep 1 - done -} - -wait_for_kubernetes_master() { - echo "Waiting for Kubernetes master to become available..." - until $($KUBECTL cluster-info &> /dev/null); do - sleep 1 - done -} - -create_kube_system_namespace() { - echo "Creating kube-system namespace..." - - $KUBECTL create -f "${HELM_ROOT}/scripts/cluster/kube-system.yaml" >/dev/null -} - -create_kube_dns() { - echo "Setting up internal dns..." - - $KUBECTL create -f "${HELM_ROOT}/scripts/cluster/skydns.yaml" >/dev/null -} - -# Generate kubeconfig data for the created cluster. -create_kubeconfig() { - local cluster_args=( - "--server=http://${KUBE_MASTER_IP}:${KUBE_PORT}" - "--insecure-skip-tls-verify=true" - ) - - kubectl config set-cluster "${KUBE_CONTEXT}" "${cluster_args[@]}" >/dev/null - kubectl config set-context "${KUBE_CONTEXT}" --cluster="${KUBE_CONTEXT}" >/dev/null - kubectl config use-context "${KUBE_CONTEXT}" > /dev/null - - echo "Wrote config for kubeconfig using context: '${KUBE_CONTEXT}'" -} - -# https://github.com/kubernetes/kubernetes/issues/23197 -# code stolen from https://github.com/huggsboson/docker-compose-kubernetes/blob/SwitchToSharedMount/kube-up.sh -cleanup_volumes() { - local machine=$(active_docker_machine) - if [ -n "$machine" ]; then - docker-machine ssh $machine "mount | grep -o 'on /var/lib/kubelet.* type' | cut -c 4- | rev | cut -c 6- | rev | sort -r | xargs --no-run-if-empty sudo umount" - docker-machine ssh $machine "sudo rm -Rf /var/lib/kubelet" - docker-machine ssh $machine "sudo mkdir -p /var/lib/kubelet" - docker-machine ssh $machine "sudo mount --bind /var/lib/kubelet /var/lib/kubelet" - docker-machine ssh $machine "sudo mount --make-shared /var/lib/kubelet" - else - mount | grep -o 'on /var/lib/kubelet.* type' | cut -c 4- | rev | cut -c 6- | rev | sort -r | xargs --no-run-if-empty sudo umount - sudo rm -Rf /var/lib/kubelet - sudo mkdir -p /var/lib/kubelet - sudo mount --bind /var/lib/kubelet /var/lib/kubelet - sudo mount --make-shared /var/lib/kubelet - fi -} - -verify_prereqs -cleanup_volumes - -if is_docker_machine; then - setup_iptables -fi - -start_kubernetes -wait_for_kubernetes_master - -create_kube_system_namespace -create_kube_dns -wait_for_kubernetes_cluster - -create_kubeconfig - - -$KUBECTL cluster-info diff --git a/scripts/kubectl.sh b/scripts/kubectl.sh deleted file mode 100755 index 12ff16f59..000000000 --- a/scripts/kubectl.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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 -eo pipefail -[[ "$TRACE" ]] && set -x - -HELM_ROOT="${BASH_SOURCE[0]%/*}/.." -source "${HELM_ROOT}/scripts/common.sh" -source "${HELM_ROOT}/scripts/docker.sh" - -KUBE_PORT=${KUBE_PORT:-8080} -KUBE_MASTER_IP=${KUBE_MASTER_IP:-localhost} - -if is_docker_machine; then - KUBE_MASTER_IP=$(docker-machine ip "$(active_docker_machine)") -fi - -kubectl -s ${KUBE_MASTER_IP}:${KUBE_PORT} "$@" diff --git a/scripts/start-local.sh b/scripts/start-local.sh deleted file mode 100755 index e533af611..000000000 --- a/scripts/start-local.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -[[ "$TRACE" ]] && set -x - -HELM_ROOT="${BASH_SOURCE[0]%/*}/.." -source "$HELM_ROOT/scripts/common.sh" - -KUBE_PROXY=${KUBE_PROXY:-} -KUBE_PROXY_PORT=${KUBE_PROXY_PORT:-8001} -MANAGER_PORT=${MANAGER_PORT:-8080} - -RESOURCIFIER=bin/resourcifier -EXPANDYBIRD=bin/expandybird -MANAGER=bin/manager - -require_binary_exists() { - if ! command -v "$1" >/dev/null 2>&1; then - error_exit "Cannot find binary for $1. Build binaries by running 'make build'" - fi -} - -kill_service() { - pkill -f "$1" || true -} - -for b in $RESOURCIFIER $EXPANDYBIRD $MANAGER; do - require_binary_exists $b - kill_service $b -done - -LOGDIR=log -if [[ ! -d $LOGDIR ]]; then - mkdir $LOGDIR -fi - -KUBECTL=$(which kubectl) || error_exit "Cannot find kubectl" - -echo "Starting resourcifier..." -nohup $RESOURCIFIER > $LOGDIR/resourcifier.log 2>&1 --kubectl="${KUBECTL}" --port=8082 & - -echo "Starting expandybird..." -nohup $EXPANDYBIRD > $LOGDIR/expandybird.log 2>&1 --port=8081 --expansion_binary=expansion/expansion.py & - -echo "Starting deployment manager..." -nohup $MANAGER > $LOGDIR/manager.log 2>&1 --port="${MANAGER_PORT}" --kubectl="${KUBECTL}" --expanderPort=8081 --deployerPort=8082 & - -if [[ "$KUBE_PROXY" ]]; then - echo "Starting kubectl proxy..." - pkill -f "$KUBECTL proxy" - nohup "$KUBECTL" proxy --port="${KUBE_PROXY_PORT}" & - sleep 1s -fi - -cat <<EOF -Local manager server is now running on :${MANAGER_PORT} - -Logging to ${LOGDIR} - -To use helm: - - export HELM_HOST=http://localhost:${MANAGER_PORT} - For list of commands, run: - $./bin/helm - Example Command: - $./bin/helm repo list - -EOF diff --git a/scripts/stop-local.sh b/scripts/stop-local.sh deleted file mode 100755 index 12ea5a45d..000000000 --- a/scripts/stop-local.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -echo "Stopping resourcifier..." -RESOURCIFIER=bin/resourcifier -pkill -f $RESOURCIFIER || echo "Resourcifier is not running" - -echo "Stopping expandybird..." -EXPANDYBIRD=bin/expandybird -pkill -f $EXPANDYBIRD || echo "Expandybird is not running" - -echo "Stopping deployment manager..." -MANAGER=bin/manager -pkill -f $MANAGER || echo "Manager is not running" diff --git a/scripts/validate-go.sh b/scripts/validate-go.sh deleted file mode 100755 index 127946386..000000000 --- a/scripts/validate-go.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# 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 -eo pipefail - -[[ "$TRACE" ]] && set -x - -readonly reset=$(tput sgr0) -readonly red=$(tput bold; tput setaf 1) -readonly green=$(tput bold; tput setaf 2) -readonly yellow=$(tput bold; tput setaf 3) - -exit_code=0 - -find_go_files() { - find . -type f -name "*.go" | grep -v vendor -} - -echo "==> Running golint..." -for pkg in $(glide nv); do - if golint_out=$(golint "$pkg" 2>&1); then - echo "${yellow}${golint_out}${reset}" - fi -done - -echo "==> Running go vet..." -echo -n "$red" -go vet $(glide nv) 2>&1 | grep -v "^exit status " || exit_code=${PIPESTATUS[0]} -echo -n "$reset" - -echo "==> Running gofmt..." -failed_fmt=$(find_go_files | xargs gofmt -s -l) -if [[ -n "${failed_fmt}" ]]; then - echo "${red}" - echo "gofmt check failed:" - echo "$failed_fmt" - find_go_files | xargs gofmt -s -d - echo "${reset}" - exit_code=1 -fi - -exit ${exit_code}