├── .dockerignore ├── .editorconfig ├── .env.development.local ├── .github └── workflows │ ├── build-agent.yaml │ ├── build-chart.yaml │ ├── build-hub.yaml │ ├── build-sdk.yaml │ ├── release-artifacts.yaml │ ├── release.yaml │ └── semantic-pr.yaml ├── .gitignore ├── .golangci.yml ├── .postcssrc.json ├── .prettierignore ├── .prettierrc.mjs ├── .release-please-manifest.json ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CHANGELOG.md ├── Dockerfile.docker-agent ├── Dockerfile.hub ├── Dockerfile.kubernetes-agent ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── angular.json ├── api ├── access_token.go ├── agent.go ├── agent_logs.go ├── application.go ├── artifact.go ├── auth.go ├── context.go ├── dashboard.go ├── deployment_target.go ├── file.go ├── tutorials.go └── user_accounts.go ├── cmd ├── agent │ ├── docker │ │ ├── agent_deployment.go │ │ ├── config.go │ │ ├── docker_actions.go │ │ ├── logs.go │ │ ├── main.go │ │ ├── metrics.go │ │ ├── self_update.go │ │ └── util.go │ └── kubernetes │ │ ├── agent_deployment.go │ │ ├── helm_actions.go │ │ ├── kubernetes_resources.go │ │ ├── logs.go │ │ ├── main.go │ │ ├── metrics.go │ │ └── status_check.go └── hub │ ├── cmd │ ├── cleanup.go │ ├── migrate.go │ ├── root.go │ └── serve.go │ ├── generate │ └── status │ │ └── generate.go │ └── main.go ├── deploy ├── charts │ └── distr │ │ ├── .helmignore │ │ ├── Chart.lock │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── README.md.gotmpl │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── cronjob.yaml │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── ingress.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ │ └── values.yaml └── docker │ ├── .env │ └── docker-compose.yaml ├── docker-compose.yaml ├── frontend └── ui │ ├── public │ ├── distr-logo.svg │ ├── docker.png │ ├── favicon.ico │ ├── favicon.svg │ ├── kubernetes.png │ └── robots.txt │ ├── src │ ├── app │ │ ├── access-tokens │ │ │ ├── access-tokens.component.html │ │ │ └── access-tokens.component.ts │ │ ├── animations │ │ │ ├── drawer.ts │ │ │ ├── dropdown.ts │ │ │ └── modal.ts │ │ ├── app-logged-in.routes.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── applications │ │ │ ├── application-detail.component.html │ │ │ ├── application-detail.component.ts │ │ │ ├── applications-page.component.html │ │ │ ├── applications-page.component.ts │ │ │ ├── applications.component.html │ │ │ └── applications.component.ts │ │ ├── artifacts │ │ │ ├── artifact-licenses │ │ │ │ ├── artifact-licenses.component.html │ │ │ │ ├── artifact-licenses.component.ts │ │ │ │ ├── edit-artifact-license.component.html │ │ │ │ └── edit-artifact-license.component.ts │ │ │ ├── artifact-pulls │ │ │ │ ├── artifact-pulls.component.html │ │ │ │ └── artifact-pulls.component.ts │ │ │ ├── artifact-versions │ │ │ │ ├── artifact-versions.component.html │ │ │ │ └── artifact-versions.component.ts │ │ │ ├── artifacts-by-customer-card │ │ │ │ ├── artifacts-by-customer-card.component.html │ │ │ │ └── artifacts-by-customer-card.component.ts │ │ │ ├── artifacts-vulnerability-report.component.ts │ │ │ ├── artifacts │ │ │ │ ├── artifacts.component.html │ │ │ │ └── artifacts.component.ts │ │ │ └── components.ts │ │ ├── components │ │ │ ├── clip.component.ts │ │ │ ├── color-scheme-switcher │ │ │ │ ├── color-scheme-switcher.component.html │ │ │ │ └── color-scheme-switcher.component.ts │ │ │ ├── confirm-dialog │ │ │ │ ├── confirm-dialog.component.html │ │ │ │ └── confirm-dialog.component.ts │ │ │ ├── connect-instructions │ │ │ │ ├── connect-instructions.component.html │ │ │ │ └── connect-instructions.component.ts │ │ │ ├── dashboard │ │ │ │ ├── dashboard.component.html │ │ │ │ └── dashboard.component.ts │ │ │ ├── editor.component.ts │ │ │ ├── home │ │ │ │ ├── home.component.html │ │ │ │ └── home.component.ts │ │ │ ├── image-upload │ │ │ │ ├── image-upload-dialog.component.html │ │ │ │ └── image-upload-dialog.component.ts │ │ │ ├── installation-wizard │ │ │ │ ├── installation-wizard-stepper.component.html │ │ │ │ ├── installation-wizard-stepper.component.ts │ │ │ │ ├── installation-wizard.component.html │ │ │ │ └── installation-wizard.component.ts │ │ │ ├── nav-bar │ │ │ │ ├── nav-bar.component.html │ │ │ │ └── nav-bar.component.ts │ │ │ ├── nav-shell.component.ts │ │ │ ├── side-bar │ │ │ │ ├── side-bar.component.html │ │ │ │ └── side-bar.component.ts │ │ │ ├── status-dot.ts │ │ │ ├── toast.component.ts │ │ │ ├── users │ │ │ │ ├── users.component.html │ │ │ │ └── users.component.ts │ │ │ └── uuid.ts │ │ ├── deployment-form │ │ │ ├── deployment-form.component.html │ │ │ └── deployment-form.component.ts │ │ ├── deployments │ │ │ ├── deployment-modal.component.ts │ │ │ ├── deployment-status-modal │ │ │ │ ├── deployment-logs-table.component.ts │ │ │ │ ├── deployment-status-modal.component.html │ │ │ │ ├── deployment-status-modal.component.ts │ │ │ │ ├── deployment-status-table.component.ts │ │ │ │ └── timeseries-table.component.ts │ │ │ ├── deployment-target-card │ │ │ │ ├── deployment-target-card.component.html │ │ │ │ ├── deployment-target-card.component.ts │ │ │ │ ├── deployment-target-metrics.component.html │ │ │ │ ├── deployment-target-metrics.component.scss │ │ │ │ └── deployment-target-metrics.component.ts │ │ │ ├── deployment-targets.component.html │ │ │ └── deployment-targets.component.ts │ │ ├── directives │ │ │ ├── autotrim.directive.ts │ │ │ └── required-role.directive.ts │ │ ├── forgot │ │ │ ├── forgot.component.html │ │ │ └── forgot.component.ts │ │ ├── invite │ │ │ ├── invite.component.html │ │ │ └── invite.component.ts │ │ ├── licenses │ │ │ ├── edit-license.component.html │ │ │ ├── edit-license.component.ts │ │ │ ├── licenses.component.html │ │ │ └── licenses.component.ts │ │ ├── login │ │ │ ├── login.component.html │ │ │ └── login.component.ts │ │ ├── organization-branding │ │ │ ├── organization-branding.component.html │ │ │ └── organization-branding.component.ts │ │ ├── organization-settings │ │ │ ├── organization-settings.component.html │ │ │ └── organization-settings.component.ts │ │ ├── password-reset │ │ │ ├── password-reset.component.html │ │ │ └── password-reset.component.ts │ │ ├── register │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── services │ │ │ ├── access-tokens.service.ts │ │ │ ├── agent-version.service.ts │ │ │ ├── applications.service.ts │ │ │ ├── artifact-licenses.service.ts │ │ │ ├── artifact-pulls.service.ts │ │ │ ├── artifacts.service.ts │ │ │ ├── auth.service.ts │ │ │ ├── cache.ts │ │ │ ├── color-scheme.service.ts │ │ │ ├── context.service.ts │ │ │ ├── dashboard.service.ts │ │ │ ├── deployment-logs.service.ts │ │ │ ├── deployment-status.service.ts │ │ │ ├── deployment-target-metrics.service.ts │ │ │ ├── deployment-targets.service.ts │ │ │ ├── error-toast.interceptor.ts │ │ │ ├── feature-flag.service.ts │ │ │ ├── files.service.ts │ │ │ ├── interfaces.ts │ │ │ ├── licenses.service.ts │ │ │ ├── markdown-options.factory.ts │ │ │ ├── organization-branding.service.ts │ │ │ ├── organization.service.ts │ │ │ ├── overlay.service.ts │ │ │ ├── settings.service.ts │ │ │ ├── sidebar.service.ts │ │ │ ├── toast.service.ts │ │ │ ├── tutorials.service.ts │ │ │ └── users.service.ts │ │ ├── tutorials │ │ │ ├── agents │ │ │ │ ├── agents-tutorial.component.html │ │ │ │ └── agents-tutorial.component.ts │ │ │ ├── branding │ │ │ │ ├── branding-tutorial.component.html │ │ │ │ └── branding-tutorial.component.ts │ │ │ ├── registry │ │ │ │ ├── registry-tutorial.component.html │ │ │ │ └── registry-tutorial.component.ts │ │ │ ├── stepper │ │ │ │ ├── tutorial-stepper.component.html │ │ │ │ └── tutorial-stepper.component.ts │ │ │ ├── tutorials.component.html │ │ │ ├── tutorials.component.ts │ │ │ └── utils.ts │ │ ├── types │ │ │ ├── application-license.ts │ │ │ ├── artifact-version-pull.ts │ │ │ ├── deployment-log-record.ts │ │ │ ├── organization.ts │ │ │ ├── timeseries-options.ts │ │ │ └── tutorials.ts │ │ └── verify │ │ │ ├── verify.component.html │ │ │ └── verify.component.ts │ ├── buildconfig │ │ ├── .gitignore │ │ └── index.ts │ ├── env │ │ ├── env.prod.ts │ │ ├── env.ts │ │ ├── remote.ts │ │ └── types.ts │ ├── index.html │ ├── main.ts │ ├── proxy.conf.json │ ├── styles.scss │ ├── styles │ │ ├── markdown.scss │ │ └── theme.scss │ └── util │ │ ├── arrays.ts │ │ ├── blob.ts │ │ ├── dates.ts │ │ ├── errors.ts │ │ ├── filter.ts │ │ ├── forms.ts │ │ ├── model.ts │ │ ├── secureImage.ts │ │ ├── slug.ts │ │ ├── units.spec.ts │ │ ├── units.ts │ │ └── validation.ts │ ├── tsconfig.app.json │ └── tsconfig.spec.json ├── go.mod ├── go.sum ├── hack ├── sentry-release.sh └── update-frontend-version.js ├── internal ├── agentauth │ └── auth.go ├── agentclient │ ├── client.go │ ├── httpstatus.go │ └── useragent │ │ └── user_agent.go ├── agentenv │ └── env.go ├── agentlogs │ ├── collector.go │ ├── exporter.go │ └── record.go ├── agentmanifest │ └── common.go ├── apierrors │ └── errors.go ├── auth │ └── authentication.go ├── authjwt │ └── jwt.go ├── authkey │ └── key.go ├── authn │ ├── authentication.go │ ├── authenticator.go │ ├── authinfo │ │ ├── authinfo.go │ │ ├── db.go │ │ ├── jwt.go │ │ ├── simple.go │ │ └── token.go │ ├── authkey │ │ └── provider.go │ ├── errors.go │ ├── jwt │ │ └── provider.go │ ├── response.go │ └── token │ │ └── token.go ├── buildconfig │ └── buildconfig.go ├── cleanup │ ├── deployment_log_records.go │ ├── deployment_revision_status.go │ ├── deployment_target_metrics.go │ └── deployment_target_status.go ├── contenttype │ ├── contenttype.go │ ├── doc.go │ └── mediatype.go ├── context │ ├── data.go │ └── main.go ├── customdomains │ └── customdomains.go ├── db │ ├── access_token.go │ ├── agent_version.go │ ├── application_license.go │ ├── applications.go │ ├── artifact_licenses.go │ ├── artifacts.go │ ├── dashboard.go │ ├── deployment_log_record.go │ ├── deployment_target_metrics.go │ ├── deployment_targets.go │ ├── deployments.go │ ├── file.go │ ├── organization.go │ ├── organization_branding.go │ ├── queryable │ │ └── interface.go │ ├── tutorials.go │ ├── tx.go │ └── user_accounts.go ├── env │ ├── env.go │ └── types.go ├── envparse │ └── envparse.go ├── envutil │ └── util.go ├── frontend │ ├── dist │ │ └── .gitignore │ └── fs.go ├── handlers │ ├── agent.go │ ├── agent_versions.go │ ├── application_licenses.go │ ├── applications.go │ ├── artifact_licenses.go │ ├── artifact_pulls.go │ ├── artifacts.go │ ├── auth.go │ ├── context.go │ ├── dashboard.go │ ├── deployment_target_metrics.go │ ├── deployment_targets.go │ ├── deployments.go │ ├── file.go │ ├── internal.go │ ├── organization.go │ ├── organization_branding.go │ ├── queryparam.go │ ├── settings.go │ ├── static.go │ ├── tutorials.go │ ├── user_accounts.go │ └── util.go ├── jobs │ ├── job.go │ ├── logger.go │ ├── runner.go │ └── scheduler.go ├── mail │ ├── mail.go │ ├── mailer.go │ ├── noop │ │ └── mailer.go │ ├── ses │ │ └── ses.go │ └── smtp │ │ └── mailer.go ├── mailsending │ ├── invite.go │ └── verify.go ├── mailtemplates │ ├── templates.go │ └── templates │ │ ├── fragments │ │ ├── footer.html │ │ ├── greeting.html │ │ ├── header.html │ │ ├── logo.html │ │ ├── signature.html │ │ └── style.html │ │ ├── invite-customer.html │ │ ├── invite-user.html │ │ ├── password-reset.html │ │ └── verify-email-registration.html ├── mapping │ ├── access_token.go │ └── list.go ├── middleware │ └── middleware.go ├── migrations │ ├── migrate.go │ └── sql │ │ ├── 0_initial.down.sql │ │ ├── 0_initial.up.sql │ │ ├── 10_deployment_revision_environment.down.sql │ │ ├── 10_deployment_revision_environment.up.sql │ │ ├── 11_deployment_user_no_cascade.down.sql │ │ ├── 11_deployment_user_no_cascade.up.sql │ │ ├── 12_application_license.down.sql │ │ ├── 12_application_license.up.sql │ │ ├── 13_org_feature_flags.down.sql │ │ ├── 13_org_feature_flags.up.sql │ │ ├── 14_deployment_revision_created_at_index.down.sql │ │ ├── 14_deployment_revision_created_at_index.up.sql │ │ ├── 15_deployment_target_created_at_index.down.sql │ │ ├── 15_deployment_target_created_at_index.up.sql │ │ ├── 16_deployment_application_no_cascade.down.sql │ │ ├── 16_deployment_application_no_cascade.up.sql │ │ ├── 17_application_version_name_unique.down.sql │ │ ├── 17_application_version_name_unique.up.sql │ │ ├── 18_application_version_archive.down.sql │ │ ├── 18_application_version_archive.up.sql │ │ ├── 19_organization_slug.down.sql │ │ ├── 19_organization_slug.up.sql │ │ ├── 1_helm_deployments.down.sql │ │ ├── 1_helm_deployments.up.sql │ │ ├── 20_artifacts.down.sql │ │ ├── 20_artifacts.up.sql │ │ ├── 21_artifact_license_expiry.down.sql │ │ ├── 21_artifact_license_expiry.up.sql │ │ ├── 22_artifact_license_org.down.sql │ │ ├── 22_artifact_license_org.up.sql │ │ ├── 23_artifacts_audit_log.down.sql │ │ ├── 23_artifacts_audit_log.up.sql │ │ ├── 24_registry_feature_flag.down.sql │ │ ├── 24_registry_feature_flag.up.sql │ │ ├── 25_artifact_size.down.sql │ │ ├── 25_artifact_size.up.sql │ │ ├── 26_organization_artifact_version_quota.down.sql │ │ ├── 26_organization_artifact_version_quota.up.sql │ │ ├── 27_deployment_revision_status_index.down.sql │ │ ├── 27_deployment_revision_status_index.up.sql │ │ ├── 28_artifact_blob_digest_indices.down.sql │ │ ├── 28_artifact_blob_digest_indices.up.sql │ │ ├── 29_user_logged_in_at.down.sql │ │ ├── 29_user_logged_in_at.up.sql │ │ ├── 2_kubernetes_deployment.down.sql │ │ ├── 2_kubernetes_deployment.up.sql │ │ ├── 30_artifacts_audit_log_ip.down.sql │ │ ├── 30_artifacts_audit_log_ip.up.sql │ │ ├── 31_status_type_progressing.down.sql │ │ ├── 31_status_type_progressing.up.sql │ │ ├── 32_images.down.sql │ │ ├── 32_images.up.sql │ │ ├── 33_tutorials.down.sql │ │ ├── 33_tutorials.up.sql │ │ ├── 34_remove_registry_feature.down.sql │ │ ├── 34_remove_registry_feature.up.sql │ │ ├── 35_docker_type.down.sql │ │ ├── 35_docker_type.up.sql │ │ ├── 36_organization_custom_domain.down.sql │ │ ├── 36_organization_custom_domain.up.sql │ │ ├── 37_deploymenttarget_metrics.down.sql │ │ ├── 37_deploymenttarget_metrics.up.sql │ │ ├── 38_fix_tutorial_docker_type.down.sql │ │ ├── 38_fix_tutorial_docker_type.up.sql │ │ ├── 39_deployment_logs.down.sql │ │ ├── 39_deployment_logs.up.sql │ │ ├── 3_remove_release_name_constraint.down.sql │ │ ├── 3_remove_release_name_constraint.up.sql │ │ ├── 40_multi_org_support.down.sql │ │ ├── 40_multi_org_support.up.sql │ │ ├── 41_pat_organization_scoped.down.sql │ │ ├── 41_pat_organization_scoped.up.sql │ │ ├── 42_tutorials_organization_scoped.down.sql │ │ ├── 42_tutorials_organization_scoped.up.sql │ │ ├── 43_file_org_nullable.down.sql │ │ ├── 43_file_org_nullable.up.sql │ │ ├── 44_remove_deployment_target_location.down.sql │ │ ├── 44_remove_deployment_target_location.up.sql │ │ ├── 4_status_type.down.sql │ │ ├── 4_status_type.up.sql │ │ ├── 5_organization_branding.down.sql │ │ ├── 5_organization_branding.up.sql │ │ ├── 6_deployment_revision.down.sql │ │ ├── 6_deployment_revision.up.sql │ │ ├── 7_agent_version.down.sql │ │ ├── 7_agent_version.up.sql │ │ ├── 8_deployment_target_scope.down.sql │ │ ├── 8_deployment_target_scope.up.sql │ │ ├── 9_access_keys.down.sql │ │ └── 9_access_keys.up.sql ├── registry │ ├── README.md │ ├── and │ │ └── and.go │ ├── audit │ │ └── audit.go │ ├── authz │ │ ├── authorizer.go │ │ └── error.go │ ├── blob.go │ ├── blob │ │ ├── error.go │ │ ├── inmemory │ │ │ └── handler.go │ │ ├── interface.go │ │ └── s3 │ │ │ └── handler.go │ ├── error.go │ ├── error │ │ └── error.go │ ├── manifest.go │ ├── manifest │ │ ├── db │ │ │ └── handler.go │ │ ├── errors.go │ │ ├── inmemory │ │ │ └── handler.go │ │ ├── interface.go │ │ └── types.go │ ├── name │ │ └── name.go │ ├── registry.go │ └── verify │ │ └── verify.go ├── resources │ ├── embedded.go │ └── embedded │ │ ├── agent │ │ ├── docker │ │ │ └── v1 │ │ │ │ └── docker-compose.yaml.tmpl │ │ └── kubernetes │ │ │ └── v1 │ │ │ └── manifest.yaml.tmpl │ │ └── apps │ │ └── hello-distr │ │ ├── docker-compose.yaml │ │ └── template.env ├── routing │ └── routing.go ├── security │ ├── password.go │ └── password_test.go ├── server │ ├── main.go │ ├── noop.go │ └── server.go ├── svc │ ├── options.go │ └── registry.go ├── types │ ├── access_token.go │ ├── agent_version.go │ ├── application.go │ ├── application_license.go │ ├── application_version.go │ ├── artifact.go │ ├── artifact_license.go │ ├── artifact_version.go │ ├── artifact_version_part.go │ ├── artifact_version_pull.go │ ├── deployment.go │ ├── deployment_log_record.go │ ├── deployment_revision.go │ ├── deployment_revision_status.go │ ├── deployment_target.go │ ├── deployment_target_status.go │ ├── file.go │ ├── organization.go │ ├── tutorials.go │ ├── types.go │ └── user_account.go ├── util │ ├── maps.go │ ├── maps_test.go │ ├── pointer.go │ ├── pointer_test.go │ └── require.go └── validation │ ├── error.go │ └── password.go ├── mise.toml ├── package-lock.json ├── package.json ├── release-please-config.json ├── renovate.json ├── sdk └── js │ ├── README.md │ ├── docs │ ├── README.md │ ├── classes │ │ ├── Client.md │ │ └── DistrService.md │ ├── interfaces │ │ ├── AccessToken.md │ │ ├── AccessTokenWithKey.md │ │ ├── AgentVersion.md │ │ ├── Application.md │ │ ├── ApplicationVersion.md │ │ ├── BaseModel.md │ │ ├── CreateAccessTokenRequest.md │ │ ├── Deployment.md │ │ ├── DeploymentRequest.md │ │ ├── DeploymentRevisionStatus.md │ │ ├── DeploymentTarget.md │ │ ├── DeploymentTargetAccessResponse.md │ │ ├── DeploymentTargetStatus.md │ │ ├── DeploymentWithLatestRevision.md │ │ ├── Named.md │ │ ├── OrganizationBranding.md │ │ ├── PatchApplicationRequest.md │ │ ├── PatchDeploymentRequest.md │ │ ├── TokenResponse.md │ │ ├── UserAccount.md │ │ └── UserAccountWithRole.md │ └── type-aliases │ │ ├── ApplicationVersionFiles.md │ │ ├── ClientConfig.md │ │ ├── CreateDeploymentParams.md │ │ ├── CreateDeploymentResult.md │ │ ├── DeploymentStatusType.md │ │ ├── DeploymentTargetScope.md │ │ ├── DeploymentType.md │ │ ├── DockerType.md │ │ ├── HelmChartType.md │ │ ├── IsOutdatedResult.md │ │ ├── LatestVersionStrategy.md │ │ ├── UpdateDeploymentParams.md │ │ └── UserRole.md │ ├── jsdoc.conf.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── client │ │ ├── client.ts │ │ ├── config.ts │ │ ├── index.ts │ │ └── service.ts │ ├── examples │ │ ├── check-deployment-outdated.ts │ │ ├── config.ts │ │ ├── create-deployment-docker.ts │ │ ├── create-deployment-kubernetes.ts │ │ ├── create-version-docker.ts │ │ ├── create-version-kubernetes.ts │ │ ├── test-client.ts │ │ ├── update-deployment-docker.ts │ │ └── update-deployment-kubernetes.ts │ ├── index.ts │ └── types │ │ ├── access-token.ts │ │ ├── agent-version.ts │ │ ├── application.ts │ │ ├── base.ts │ │ ├── deployment-target.ts │ │ ├── deployment.ts │ │ ├── index.ts │ │ ├── organization-branding.ts │ │ └── user-account.ts │ ├── tsconfig.json │ └── typedoc.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | internal/frontend/dist 3 | deploy 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | 19 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 20 | indent_style = tab 21 | tab_width = 4 22 | -------------------------------------------------------------------------------- /.github/workflows/release-artifacts.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Release Artifacts 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | build: 12 | name: Upload deploy-docker.tar.bz2 13 | timeout-minutes: 5 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | - name: Create Archive 21 | run: tar -caf deploy-docker.tar.bz2 -C deploy/docker/ . 22 | - name: Upload Archive 23 | run: gh release upload ${{ github.ref_name }} deploy-docker.tar.bz2 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release-please: 11 | timeout-minutes: 1 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | outputs: 17 | releases_created: ${{ steps.release-please.outputs.releases_created }} 18 | tag_name: ${{ steps.release-please.outputs.tag_name }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Release Please 23 | id: release-please 24 | uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 25 | with: 26 | token: ${{ secrets.GLASSKUBE_BOT_SECRET }} 27 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Check Pull Request Title 4 | 5 | on: 6 | pull_request_target: 7 | types: 8 | - opened 9 | - edited 10 | - synchronize 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | semantic-message: 17 | timeout-minutes: 1 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | sdk/js/node_modules 15 | sdk/js/dist 16 | 17 | # IDEs and editors 18 | .idea/ 19 | *.iml 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # Visual Studio Code 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # Miscellaneous 36 | /.angular/cache 37 | .sass-cache/ 38 | /connect.lock 39 | /coverage 40 | /libpeerconnection.log 41 | testem.log 42 | /typings 43 | 44 | # System files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | # Sentry Config File 49 | .sentryclirc 50 | 51 | *.tgz 52 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | run: 3 | allow-parallel-runners: true 4 | issues: 5 | max-issues-per-linter: 100 6 | max-same-issues: 100 7 | linters: 8 | enable: 9 | - asasalint 10 | - asciicheck 11 | - bidichk 12 | - contextcheck 13 | - copyloopvar 14 | - decorder 15 | - dogsled 16 | - dupl 17 | - dupword 18 | - errcheck 19 | - errchkjson 20 | - errname 21 | - ginkgolinter 22 | - gocheckcompilerdirectives 23 | - goconst 24 | - gocyclo 25 | - goprintffuncname 26 | - govet 27 | - importas 28 | - ineffassign 29 | - lll 30 | - loggercheck 31 | - makezero 32 | - mirror 33 | - misspell 34 | - nakedret 35 | - prealloc 36 | - staticcheck 37 | - unconvert 38 | - unparam 39 | - unused 40 | - whitespace 41 | exclusions: 42 | generated: lax 43 | presets: 44 | - comments 45 | - common-false-positives 46 | - legacy 47 | - std-error-handling 48 | paths: 49 | - third_party$ 50 | - builtin$ 51 | - examples$ 52 | formatters: 53 | enable: 54 | - gci 55 | - gofmt 56 | - gofumpt 57 | - goimports 58 | exclusions: 59 | generated: lax 60 | paths: 61 | - third_party$ 62 | - builtin$ 63 | - examples$ 64 | -------------------------------------------------------------------------------- /.postcssrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@tailwindcss/postcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | **/dist/* 3 | CHANGELOG.md 4 | deploy/charts/*/templates/* 5 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = { 6 | bracketSameLine: true, 7 | bracketSpacing: false, 8 | singleQuote: true, 9 | semi: true, 10 | tabWidth: 2, 11 | printWidth: 120, 12 | trailingComma: 'es5', 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.11.1" 3 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Dockerfile.docker-agent: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 AS builder 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | ARG VERSION 5 | ARG COMMIT 6 | 7 | WORKDIR /workspace 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | RUN go mod download 11 | 12 | COPY api/ api/ 13 | COPY cmd/agent/docker/ cmd/agent/docker/ 14 | # doesn't exist (yet?) 15 | # COPY pkg/ pkg/ 16 | COPY internal/ internal/ 17 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ 18 | go build -a -o agent \ 19 | -ldflags="-s -w -X github.com/glasskube/distr/internal/buildconfig.version=${VERSION:-snapshot} -X github.com/glasskube/distr/internal/buildconfig.commit=${COMMIT}" \ 20 | ./cmd/agent/docker/ 21 | 22 | FROM docker:27.3.1-alpine3.20 23 | WORKDIR / 24 | COPY --from=builder /workspace/agent . 25 | 26 | ENTRYPOINT ["/agent"] 27 | -------------------------------------------------------------------------------- /Dockerfile.hub: -------------------------------------------------------------------------------- 1 | # Use distroless as minimal base image to package the manager binary 2 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 3 | FROM gcr.io/distroless/static-debian12:nonroot@sha256:188ddfb9e497f861177352057cb21913d840ecae6c843d39e00d44fa64daa51c 4 | WORKDIR / 5 | COPY dist/distr /distr 6 | USER 65532:65532 7 | ENTRYPOINT ["/distr"] 8 | CMD ["serve"] 9 | -------------------------------------------------------------------------------- /Dockerfile.kubernetes-agent: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 AS builder 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | ARG VERSION 5 | ARG COMMIT 6 | 7 | WORKDIR /workspace 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | RUN go mod download 11 | 12 | COPY api/ api/ 13 | COPY cmd/agent/kubernetes/ cmd/agent/kubernetes/ 14 | # doesn't exist (yet?) 15 | # COPY pkg/ pkg/ 16 | COPY internal/ internal/ 17 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ 18 | go build -a -o agent \ 19 | -ldflags="-s -w -X github.com/glasskube/distr/internal/buildconfig.version=${VERSION:-snapshot} -X github.com/glasskube/distr/internal/buildconfig.commit=${COMMIT}" \ 20 | ./cmd/agent/kubernetes/ 21 | 22 | FROM gcr.io/distroless/static-debian12:nonroot@sha256:188ddfb9e497f861177352057cb21913d840ecae6c843d39e00d44fa64daa51c 23 | WORKDIR / 24 | COPY --from=builder /workspace/agent . 25 | USER 65532:65532 26 | ENTRYPOINT ["/agent"] 27 | -------------------------------------------------------------------------------- /api/access_token.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/glasskube/distr/internal/authkey" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type AccessToken struct { 11 | ID uuid.UUID `json:"id"` 12 | CreatedAt time.Time `json:"createdAt"` 13 | ExpiresAt *time.Time `json:"expiresAt,omitempty"` 14 | LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` 15 | Label *string `json:"label,omitempty"` 16 | } 17 | 18 | func (obj AccessToken) WithKey(key authkey.Key) AccessTokenWithKey { 19 | return AccessTokenWithKey{obj, key} 20 | } 21 | 22 | type AccessTokenWithKey struct { 23 | AccessToken 24 | Key authkey.Key `json:"key"` 25 | } 26 | 27 | type CreateAccessTokenRequest struct { 28 | ExpiresAt *time.Time `json:"expiresAt"` 29 | Label *string `json:"label"` 30 | } 31 | -------------------------------------------------------------------------------- /api/agent_logs.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type DeploymentLogRecord struct { 10 | DeploymentID uuid.UUID `json:"deploymentId"` 11 | DeploymentRevisionID uuid.UUID `json:"deploymentRevisionId"` 12 | Resource string `json:"resource"` 13 | Timestamp time.Time `json:"timestamp"` 14 | Severity string `json:"severity"` 15 | Body string `json:"body"` 16 | } 17 | -------------------------------------------------------------------------------- /api/application.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/glasskube/distr/internal/types" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type ApplicationResponse struct { 11 | types.Application 12 | ImageUrl string `json:"imageUrl"` 13 | } 14 | 15 | func AsApplication(a types.Application) ApplicationResponse { 16 | return ApplicationResponse{ 17 | Application: a, 18 | ImageUrl: WithImageUrl(a.ImageID), 19 | } 20 | } 21 | 22 | type ApplicationsResponse struct { 23 | types.Application 24 | ImageUrl string `json:"imageUrl"` 25 | } 26 | 27 | func AsApplications(a types.Application) ApplicationsResponse { 28 | return ApplicationsResponse{ 29 | Application: a, 30 | ImageUrl: WithImageUrl(a.ImageID), 31 | } 32 | } 33 | 34 | func MapApplicationsToResponse(applications []types.Application) []ApplicationsResponse { 35 | result := make([]ApplicationsResponse, len(applications)) 36 | for i, a := range applications { 37 | result[i] = AsApplications(a) 38 | } 39 | return result 40 | } 41 | 42 | type PatchApplicationRequest struct { 43 | Name *string `json:"name,omitempty"` 44 | Versions []PatchApplicationVersionRequest `json:"versions,omitempty"` 45 | } 46 | 47 | type PatchApplicationVersionRequest struct { 48 | ID uuid.UUID `json:"id"` 49 | ArchivedAt *time.Time `json:"archivedAt,omitempty"` 50 | } 51 | -------------------------------------------------------------------------------- /api/artifact.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/glasskube/distr/internal/types" 4 | 5 | type ArtifactResponse struct { 6 | types.ArtifactWithTaggedVersion 7 | ImageUrl string `json:"imageUrl"` 8 | } 9 | 10 | func AsArtifact(a types.ArtifactWithTaggedVersion) ArtifactResponse { 11 | return ArtifactResponse{ 12 | ArtifactWithTaggedVersion: a, 13 | ImageUrl: WithImageUrl(a.ImageID), 14 | } 15 | } 16 | 17 | type ArtifactsResponse struct { 18 | types.ArtifactWithDownloads 19 | ImageUrl string `json:"imageUrl"` 20 | } 21 | 22 | func AsArtifacts(a types.ArtifactWithDownloads) ArtifactsResponse { 23 | return ArtifactsResponse{ 24 | ArtifactWithDownloads: a, 25 | ImageUrl: WithImageUrl(a.ImageID), 26 | } 27 | } 28 | 29 | func MapArtifactsToResponse(artifacts []types.ArtifactWithDownloads) []ArtifactsResponse { 30 | result := make([]ArtifactsResponse, len(artifacts)) 31 | for i, a := range artifacts { 32 | result[i] = AsArtifacts(a) 33 | } 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /api/auth.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/glasskube/distr/internal/validation" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type AuthLoginRequest struct { 9 | Email string `json:"email"` 10 | Password string `json:"password"` 11 | } 12 | 13 | type AuthLoginResponse struct { 14 | Token string `json:"token"` 15 | } 16 | 17 | type AuthRegistrationRequest struct { 18 | Name string `json:"name"` 19 | Email string `json:"email"` 20 | Password string `json:"password"` 21 | } 22 | 23 | func (r *AuthRegistrationRequest) Validate() error { 24 | if r.Email == "" { 25 | return validation.NewValidationFailedError("email is empty") 26 | } else if err := validation.ValidatePassword(r.Password); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | type AuthResetPasswordRequest struct { 33 | Email string `json:"email"` 34 | } 35 | 36 | func (r *AuthResetPasswordRequest) Validate() error { 37 | if r.Email == "" { 38 | return validation.NewValidationFailedError("email is empty") 39 | } 40 | return nil 41 | } 42 | 43 | type AuthSwitchContextRequest struct { 44 | OrganizationID uuid.UUID `json:"organizationId"` 45 | } 46 | -------------------------------------------------------------------------------- /api/context.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/glasskube/distr/internal/types" 4 | 5 | type ContextResponse struct { 6 | User UserAccountResponse `json:"user"` 7 | Organization types.Organization `json:"organization"` 8 | AvailableContexts []types.OrganizationWithUserRole `json:"availableContexts,omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /api/dashboard.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type DashboardArtifact struct { 4 | Artifact ArtifactResponse `json:"artifact"` 5 | LatestPulledVersion string `json:"latestPulledVersion"` 6 | } 7 | 8 | type ArtifactsByCustomer struct { 9 | Customer UserAccountResponse `json:"customer"` 10 | Artifacts []DashboardArtifact `json:"artifacts,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /api/file.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type PatchImageRequest struct { 8 | ImageID uuid.UUID `json:"imageId"` 9 | } 10 | 11 | func WithImageUrl(imageID *uuid.UUID) string { 12 | if imageID == nil || uuid.Nil == *imageID { 13 | return "" 14 | } 15 | return "/api/v1/files/" + imageID.String() 16 | } 17 | -------------------------------------------------------------------------------- /api/tutorials.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/glasskube/distr/internal/types" 4 | 5 | type TutorialProgressRequest struct { 6 | types.TutorialProgressEvent 7 | MarkCompleted bool `json:"markCompleted"` 8 | } 9 | -------------------------------------------------------------------------------- /cmd/agent/docker/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | func ScratchDir() string { 6 | if dir := os.Getenv("DISTR_AGENT_SCRATCH_DIR"); dir != "" { 7 | return dir 8 | } 9 | return "./scratch" 10 | } 11 | -------------------------------------------------------------------------------- /cmd/agent/docker/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "gopkg.in/yaml.v3" 4 | 5 | func DecodeComposeFile(manifest []byte) (result map[string]any, err error) { 6 | err = yaml.Unmarshal(manifest, &result) 7 | return 8 | } 9 | 10 | func EncodeComposeFile(compose map[string]any) (result []byte, err error) { 11 | return yaml.Marshal(compose) 12 | } 13 | -------------------------------------------------------------------------------- /cmd/hub/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/glasskube/distr/internal/buildconfig" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var RootCommand = &cobra.Command{ 9 | Use: "distr", 10 | Version: buildconfig.Version(), 11 | } 12 | -------------------------------------------------------------------------------- /cmd/hub/generate/status/generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | internalctx "github.com/glasskube/distr/internal/context" 8 | "github.com/glasskube/distr/internal/db" 9 | "github.com/glasskube/distr/internal/env" 10 | "github.com/glasskube/distr/internal/svc" 11 | "github.com/glasskube/distr/internal/types" 12 | "github.com/glasskube/distr/internal/util" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | func main() { 17 | ctx := context.Background() 18 | env.Initialize() 19 | registry := util.Require(svc.NewDefault(ctx)) 20 | defer func() { _ = registry.Shutdown(ctx) }() 21 | ctx = internalctx.WithDb(ctx, registry.GetDbPool()) 22 | 23 | revisionID := uuid.MustParse("fb3e0293-a782-4088-a50c-ec43bee8f03d") 24 | statusCount := 2000000 25 | statusInterval := 5 * time.Second 26 | 27 | now := time.Now().UTC() 28 | createdAt := now.Add(time.Duration(statusCount) * -statusInterval) 29 | var ds []types.DeploymentRevisionStatus 30 | for createdAt.Before(now) { 31 | ds = append(ds, types.DeploymentRevisionStatus{CreatedAt: createdAt, Message: "demo status"}) 32 | createdAt = createdAt.Add(statusInterval) 33 | } 34 | util.Must(db.BulkCreateDeploymentRevisionStatusWithCreatedAt(ctx, revisionID, ds)) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/hub/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/glasskube/distr/cmd/hub/cmd" 7 | ) 8 | 9 | func main() { 10 | if err := cmd.RootCommand.Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deploy/charts/distr/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/charts/distr/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: postgresql 3 | repository: oci://registry-1.docker.io/bitnamicharts 4 | version: 16.5.6 5 | - name: minio 6 | repository: https://charts.min.io 7 | version: 5.4.0 8 | digest: sha256:359f384b58c34b0da853053174a8870d62cc6b4d4a602e40701c6822e7ea25af 9 | generated: "2025-03-25T15:50:00.196141878+01:00" 10 | -------------------------------------------------------------------------------- /deploy/charts/distr/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: distr 3 | description: The easiest way to distribute enterprise software 4 | keywords: 5 | - distr 6 | - software distribution 7 | - on-prem management 8 | - docker 9 | - kubernetes 10 | maintainers: 11 | - name: Glasskube 12 | url: https://github.com/glasskube 13 | icon: >- 14 | https://github.com/glasskube/distr/raw/refs/heads/main/frontend/ui/public/distr-logo.svg 15 | home: https://distr.sh/docs/ 16 | type: application 17 | version: 1.11.1 18 | appVersion: 1.11.1 19 | dependencies: 20 | - name: postgresql 21 | repository: oci://registry-1.docker.io/bitnamicharts 22 | version: 16.x.x 23 | condition: postgresql.enabled 24 | - name: minio 25 | repository: https://charts.min.io 26 | version: 5.x.x 27 | condition: minio.enabled 28 | -------------------------------------------------------------------------------- /deploy/charts/distr/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "distr.fullname" . }} 6 | labels: 7 | {{- include "distr.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "distr.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /deploy/charts/distr/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "distr.fullname" . }} 6 | labels: 7 | {{- include "distr.labels" . | nindent 4 }} 8 | {{- with .Values.ingress.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | {{- with .Values.ingress.className }} 14 | ingressClassName: {{ . }} 15 | {{- end }} 16 | {{- if .Values.ingress.tls }} 17 | tls: 18 | {{- range .Values.ingress.tls }} 19 | - hosts: 20 | {{- range .hosts }} 21 | - {{ . | quote }} 22 | {{- end }} 23 | secretName: {{ .secretName }} 24 | {{- end }} 25 | {{- end }} 26 | rules: 27 | {{- range .Values.ingress.hosts }} 28 | - host: {{ .host | quote }} 29 | http: 30 | paths: 31 | {{- range .paths }} 32 | - path: {{ .path }} 33 | {{- with .pathType }} 34 | pathType: {{ . }} 35 | {{- end }} 36 | backend: 37 | service: 38 | name: {{ include "distr.fullname" $ }} 39 | port: 40 | {{- toYaml .port | nindent 18 }} 41 | {{- end }} 42 | {{- end }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /deploy/charts/distr/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "distr.fullname" . }} 5 | labels: 6 | {{- include "distr.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | - port: {{ .Values.service.artifactsPort }} 15 | targetPort: artifacts 16 | protocol: TCP 17 | name: artifacts 18 | selector: 19 | {{- include "distr.selectorLabels" . | nindent 4 }} 20 | -------------------------------------------------------------------------------- /deploy/charts/distr/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "distr.serviceAccountName" . }} 6 | labels: 7 | {{- include "distr.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /deploy/charts/distr/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "distr.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "distr.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "distr.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: distr-dev 2 | 3 | services: 4 | postgres: 5 | image: 'postgres:17.5-alpine3.20' 6 | environment: 7 | POSTGRES_USER: local 8 | POSTGRES_PASSWORD: local 9 | POSTGRES_DB: distr 10 | volumes: 11 | - 'postgres:/var/lib/postgresql/data/' 12 | ports: 13 | - '5432:5432' 14 | mailpit: 15 | image: 'axllent/mailpit:v1.25.1' 16 | ports: 17 | - '1025:1025' 18 | - '8025:8025' 19 | minio: 20 | image: 'minio/minio:RELEASE.2025-02-28T09-55-16Z' 21 | entrypoint: sh 22 | command: 23 | - -c 24 | - mkdir -p /data/distr && minio server /data --console-address :9001 25 | ports: 26 | - 9000:9000 27 | - 9001:9001 28 | environment: 29 | - 'MINIO_ROOT_USER=distr' 30 | - 'MINIO_ROOT_PASSWORD=distr123' 31 | volumes: 32 | - minio:/data/ 33 | 34 | volumes: 35 | postgres: 36 | minio: 37 | -------------------------------------------------------------------------------- /frontend/ui/public/distr-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/ui/public/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glasskube/distr/9b1bbd751bbbb0e43a6ebf0dd946467103799800/frontend/ui/public/docker.png -------------------------------------------------------------------------------- /frontend/ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glasskube/distr/9b1bbd751bbbb0e43a6ebf0dd946467103799800/frontend/ui/public/favicon.ico -------------------------------------------------------------------------------- /frontend/ui/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/ui/public/kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glasskube/distr/9b1bbd751bbbb0e43a6ebf0dd946467103799800/frontend/ui/public/kubernetes.png -------------------------------------------------------------------------------- /frontend/ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | Allow: /login 4 | -------------------------------------------------------------------------------- /frontend/ui/src/app/animations/drawer.ts: -------------------------------------------------------------------------------- 1 | import {trigger, state, style, transition, animate} from '@angular/animations'; 2 | 3 | export const drawerFlyInOut = trigger('drawerFlyInOut', [ 4 | state('in', style({transform: 'translateX(0)', opacity: '1'})), 5 | transition('void => *', [style({transform: 'translateX(100%)', opacity: '0.8'}), animate('150ms ease-out')]), 6 | transition('* => void', [animate('150ms ease-in', style({transform: 'translateX(100%)', opacity: '0.8'}))]), 7 | ]); 8 | -------------------------------------------------------------------------------- /frontend/ui/src/app/animations/dropdown.ts: -------------------------------------------------------------------------------- 1 | import {animate, state, style, transition, trigger} from '@angular/animations'; 2 | 3 | export const dropdownAnimation = trigger('dropdown', [ 4 | state('in', style({transform: 'rotateX(0)'})), 5 | transition('void => *', [style({transform: 'rotateX(-90deg)'}), animate('100ms ease-out')]), 6 | transition('* => void', [animate('100ms ease-in', style({transform: 'rotateX(-90deg)'}))]), 7 | ]); 8 | -------------------------------------------------------------------------------- /frontend/ui/src/app/animations/modal.ts: -------------------------------------------------------------------------------- 1 | import {trigger, state, style, transition, animate} from '@angular/animations'; 2 | 3 | export const modalFlyInOut = trigger('modalFlyInOut', [ 4 | state('visible', style({transform: 'translateY(0)', opacity: '1'})), 5 | state('hidden', style({transform: 'translateY(-100%)', opacity: '0.8'})), 6 | transition('void => *', [style({transform: 'translateY(-100%)', opacity: '0.8'}), animate('150ms ease-out')]), 7 | transition('* => void', [animate('150ms ease-in', style({transform: 'translateY(-100%)', opacity: '0.8'}))]), 8 | transition('visible <=> hidden', [animate('150ms ease-in')]), 9 | ]); 10 | -------------------------------------------------------------------------------- /frontend/ui/src/app/applications/applications-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /frontend/ui/src/app/applications/applications-page.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ApplicationsComponent} from './applications.component'; 3 | 4 | @Component({ 5 | selector: 'app-applications-page', 6 | standalone: true, 7 | imports: [ApplicationsComponent], 8 | templateUrl: './applications-page.component.html', 9 | }) 10 | export class ApplicationsPageComponent {} 11 | -------------------------------------------------------------------------------- /frontend/ui/src/app/components/clip.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, inject, input} from '@angular/core'; 2 | import {FaIconComponent} from '@fortawesome/angular-fontawesome'; 3 | import {faClipboard, faClipboardCheck} from '@fortawesome/free-solid-svg-icons'; 4 | import {ToastService} from '../services/toast.service'; 5 | 6 | @Component({ 7 | selector: 'app-clip', 8 | imports: [FaIconComponent], 9 | template: ` 10 | 21 | `, 22 | }) 23 | export class ClipComponent { 24 | public readonly clip = input.required(); 25 | 26 | private readonly toast = inject(ToastService); 27 | 28 | protected readonly faClipboard = faClipboard; 29 | protected readonly faClipboardCheck = faClipboardCheck; 30 | 31 | protected copied = false; 32 | 33 | public async writeClip() { 34 | await navigator.clipboard.writeText(this.clip()); 35 | this.toast.success('copied to clipboard'); 36 | this.copied = true; 37 | setTimeout(() => (this.copied = false), 2000); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/ui/src/app/components/color-scheme-switcher/color-scheme-switcher.component.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /frontend/ui/src/app/components/color-scheme-switcher/color-scheme-switcher.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, inject} from '@angular/core'; 2 | import {NgIf} from '@angular/common'; 3 | import {ColorSchemeService} from '../../services/color-scheme.service'; 4 | import {FaIconComponent} from '@fortawesome/angular-fontawesome'; 5 | import {faMoon, faSun} from '@fortawesome/free-solid-svg-icons'; 6 | 7 | @Component({ 8 | selector: 'app-color-scheme-switcher', 9 | standalone: true, 10 | templateUrl: './color-scheme-switcher.component.html', 11 | imports: [NgIf, FaIconComponent], 12 | }) 13 | export class ColorSchemeSwitcherComponent { 14 | private colorSchemeService = inject(ColorSchemeService); 15 | public colorSchemeSignal = this.colorSchemeService.colorScheme; 16 | 17 | protected readonly faSun = faSun; 18 | protected readonly faMoon = faMoon; 19 | 20 | constructor() {} 21 | 22 | switchColorScheme() { 23 | let newColorScheme: 'dark' | '' = 'dark'; 24 | if ('dark' === this.colorSchemeSignal()) { 25 | newColorScheme = ''; 26 | } 27 | this.colorSchemeSignal.set(newColorScheme); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/ui/src/app/components/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @if (brandingDescription$ | async; as description) { 4 |
5 | } @else { 6 |
7 | Homepage not yet configured by vendor in branding settings 8 |
9 | } 10 |
11 |
12 | -------------------------------------------------------------------------------- /frontend/ui/src/app/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {AsyncPipe} from '@angular/common'; 2 | import {Component, inject} from '@angular/core'; 3 | import {catchError, EMPTY, map, Observable} from 'rxjs'; 4 | import {OrganizationBrandingService} from '../../services/organization-branding.service'; 5 | import {MarkdownPipe} from 'ngx-markdown'; 6 | import {HttpErrorResponse} from '@angular/common/http'; 7 | import {getFormDisplayedError} from '../../../util/errors'; 8 | import {ToastService} from '../../services/toast.service'; 9 | 10 | @Component({ 11 | selector: 'app-home', 12 | imports: [AsyncPipe, MarkdownPipe], 13 | templateUrl: './home.component.html', 14 | }) 15 | export class HomeComponent { 16 | private readonly organizationBranding = inject(OrganizationBrandingService); 17 | private toast = inject(ToastService); 18 | readonly brandingDescription$: Observable = this.organizationBranding.get().pipe( 19 | catchError((e) => { 20 | const msg = getFormDisplayedError(e); 21 | if (msg && e instanceof HttpErrorResponse && e.status !== 404) { 22 | this.toast.error(msg); 23 | } 24 | return EMPTY; 25 | }), 26 | map((b) => b.description) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/ui/src/app/components/nav-shell.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {RouterOutlet} from '@angular/router'; 3 | import {NavBarComponent} from './nav-bar/nav-bar.component'; 4 | import {SideBarComponent} from './side-bar/side-bar.component'; 5 | 6 | @Component({ 7 | selector: 'app-nav-shell', 8 | template: ` 9 | 10 | 11 | 12 | `, 13 | imports: [NavBarComponent, SideBarComponent, RouterOutlet], 14 | }) 15 | export class NavShellComponent {} 16 | -------------------------------------------------------------------------------- /frontend/ui/src/app/deployments/deployment-target-card/deployment-target-metrics.component.scss: -------------------------------------------------------------------------------- 1 | .gauge { 2 | position: relative; 3 | width: 40px; 4 | aspect-ratio: 1; 5 | border-radius: 50%; 6 | background: conic-gradient(green 0%, yellow 50%, red 100%); 7 | 8 | @for $i from 0 through 100 { 9 | @if ($i % 5 == 0) { 10 | &.percent-#{$i} { 11 | $deg: ($i * 3.6); 12 | mask: conic-gradient(white 0deg #{$deg}deg, #00000036 #{$deg}deg 360deg); 13 | } 14 | } 15 | } 16 | } 17 | 18 | .gauge-center { 19 | position: absolute; 20 | top: 10%; 21 | left: 10%; 22 | width: 80%; 23 | height: 80%; 24 | border-radius: 50%; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/ui/src/app/directives/autotrim.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, HostListener, inject} from '@angular/core'; 2 | import {NgControl} from '@angular/forms'; 3 | 4 | @Directive({selector: 'input[autotrim]'}) 5 | export class AutotrimDirective { 6 | private readonly ngControl = inject(NgControl, {optional: true}); 7 | 8 | @HostListener('blur', ['$event']) onBlur(event: Event) { 9 | const target = event.target as HTMLInputElement; 10 | if (target.value !== target.value.trim()) { 11 | target.value = target.value.trim(); 12 | } 13 | if (typeof this.ngControl?.value === 'string' && this.ngControl.value !== this.ngControl.value.trim()) { 14 | this.ngControl.control?.setValue(this.ngControl.value.trim()); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/ui/src/app/directives/required-role.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | EmbeddedViewRef, 4 | inject, 5 | Input, 6 | OnChanges, 7 | SimpleChanges, 8 | TemplateRef, 9 | ViewContainerRef, 10 | } from '@angular/core'; 11 | import {AuthService} from '../services/auth.service'; 12 | import {UserRole} from '@glasskube/distr-sdk'; 13 | 14 | @Directive({ 15 | selector: '[appRequiredRole]', 16 | }) 17 | export class RequireRoleDirective implements OnChanges { 18 | private readonly auth = inject(AuthService); 19 | private readonly templateRef = inject(TemplateRef); 20 | private readonly viewContainerRef = inject(ViewContainerRef); 21 | private embeddedViewRef: EmbeddedViewRef | null = null; 22 | @Input({required: true, alias: 'appRequiredRole'}) public role!: UserRole; 23 | 24 | public ngOnChanges(changes: SimpleChanges): void { 25 | if (changes['role']) { 26 | if (this.auth.hasRole(this.role)) { 27 | if (this.embeddedViewRef === null) { 28 | this.embeddedViewRef = this.viewContainerRef.createEmbeddedView(this.templateRef); 29 | } 30 | } else { 31 | if (this.embeddedViewRef !== null) { 32 | this.embeddedViewRef.destroy(); 33 | this.embeddedViewRef = null; 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/access-tokens.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {Observable} from 'rxjs'; 4 | import {AccessToken, AccessTokenWithKey, CreateAccessTokenRequest} from '@glasskube/distr-sdk'; 5 | 6 | const baseUrl = '/api/v1/settings/tokens'; 7 | 8 | @Injectable({providedIn: 'root'}) 9 | export class AccessTokensService { 10 | private readonly httpClient = inject(HttpClient); 11 | 12 | public list(): Observable { 13 | return this.httpClient.get(baseUrl); 14 | } 15 | 16 | public create(request: CreateAccessTokenRequest): Observable { 17 | return this.httpClient.post(baseUrl, request); 18 | } 19 | 20 | public delete(id: string): Observable { 21 | return this.httpClient.delete(`${baseUrl}/${id}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/agent-version.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {Observable, shareReplay} from 'rxjs'; 4 | import {AgentVersion} from '@glasskube/distr-sdk'; 5 | 6 | const baseUrl = '/api/v1/agent-versions'; 7 | 8 | @Injectable({providedIn: 'root'}) 9 | export class AgentVersionService { 10 | private readonly httpClient = inject(HttpClient); 11 | private readonly agentVersions$ = this.httpClient.get(baseUrl).pipe(shareReplay(1)); 12 | 13 | public list(): Observable { 14 | return this.agentVersions$; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/artifact-pulls.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient, HttpParams} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {Observable} from 'rxjs'; 4 | import {ArtifactVersionPull} from '../types/artifact-version-pull'; 5 | 6 | @Injectable({providedIn: 'root'}) 7 | export class ArtifactPullsService { 8 | private readonly baseUrl = '/api/v1/artifact-pulls'; 9 | private readonly httpClient = inject(HttpClient); 10 | 11 | public get({before, count}: {before?: Date; count?: number} = {}): Observable { 12 | let params = new HttpParams(); 13 | if (before !== undefined) { 14 | params = params.set('before', before.toISOString()); 15 | } 16 | if (count !== undefined) { 17 | params = params.set('count', count); 18 | } 19 | return this.httpClient.get(this.baseUrl, {params}); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/color-scheme.service.ts: -------------------------------------------------------------------------------- 1 | import {effect, Injectable, signal, WritableSignal} from '@angular/core'; 2 | import {fromEvent} from 'rxjs'; 3 | import {toSignal} from '@angular/core/rxjs-interop'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ColorSchemeService { 9 | private COLOR_SCHEME = 'COLOR_SCHEME'; 10 | 11 | // syncs color scheme across tabs 12 | private storageSignal = toSignal(fromEvent(window, 'storage')); 13 | private _colorSchemeSignal: WritableSignal<'dark' | ''> = signal(this.readColorSchemeFromLocalStorage()); 14 | 15 | public get colorScheme() { 16 | return this._colorSchemeSignal; 17 | } 18 | 19 | constructor() { 20 | effect(() => { 21 | window.localStorage[this.COLOR_SCHEME] = this.colorScheme(); 22 | }); 23 | effect(() => { 24 | this.storageSignal(); 25 | this.colorScheme.set(this.readColorSchemeFromLocalStorage()); 26 | }); 27 | } 28 | 29 | private readColorSchemeFromLocalStorage() { 30 | switch (window.localStorage[this.COLOR_SCHEME]) { 31 | case '': 32 | return ''; 33 | case 'dark': 34 | return 'dark'; 35 | default: 36 | if (window && window.matchMedia('(prefers-color-scheme: dark)').matches) { 37 | return 'dark'; 38 | } 39 | } 40 | return ''; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/dashboard.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {Observable} from 'rxjs'; 4 | import {UserAccountWithRole} from '@glasskube/distr-sdk'; 5 | import {ArtifactWithTags} from './artifacts.service'; 6 | 7 | export interface DashboardArtifact { 8 | artifact: ArtifactWithTags; 9 | latestPulledVersion: string; 10 | } 11 | 12 | export interface ArtifactsByCustomer { 13 | customer: UserAccountWithRole; 14 | artifacts?: DashboardArtifact[]; 15 | } 16 | 17 | @Injectable({providedIn: 'root'}) 18 | export class DashboardService { 19 | private readonly httpClient = inject(HttpClient); 20 | private readonly baseUrl = '/api/v1/dashboard'; 21 | 22 | public getArtifactsByCustomer(): Observable { 23 | return this.httpClient.get(`${this.baseUrl}/artifacts-by-customer`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/deployment-logs.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {Observable} from 'rxjs'; 4 | import {DeploymentLogRecord} from '../types/deployment-log-record'; 5 | import {TimeseriesOptions, timeseriesOptionsAsParams} from '../types/timeseries-options'; 6 | 7 | @Injectable({providedIn: 'root'}) 8 | export class DeploymentLogsService { 9 | private readonly httpClient = inject(HttpClient); 10 | 11 | public getResources(deploymentId: string): Observable { 12 | return this.httpClient.get(`/api/v1/deployments/${deploymentId}/logs/resources`); 13 | } 14 | 15 | public get(deploymentId: string, resource: string, options?: TimeseriesOptions): Observable { 16 | const params = {resource, ...timeseriesOptionsAsParams(options)}; 17 | return this.httpClient.get(`/api/v1/deployments/${deploymentId}/logs`, {params}); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/deployment-status.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {DeploymentRevisionStatus} from '@glasskube/distr-sdk'; 4 | import {Observable, switchMap, timer} from 'rxjs'; 5 | import {TimeseriesOptions, timeseriesOptionsAsParams} from '../types/timeseries-options'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class DeploymentStatusService { 11 | private readonly baseUrl = '/api/v1/deployments'; 12 | private readonly httpClient = inject(HttpClient); 13 | 14 | getStatuses(deploymentId: string, options?: TimeseriesOptions): Observable { 15 | const params = timeseriesOptionsAsParams(options); 16 | return this.httpClient.get(`${this.baseUrl}/${deploymentId}/status`, {params}); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/deployment-target-metrics.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {merge, Observable, shareReplay, Subject, switchMap, tap, timer} from 'rxjs'; 4 | import {DefaultReactiveList} from './cache'; 5 | 6 | interface AgentDeploymentTargetMetrics { 7 | cpuCoresMillis: number; 8 | cpuUsage: number; 9 | memoryBytes: number; 10 | memoryUsage: number; 11 | } 12 | 13 | export interface DeploymentTargetLatestMetrics extends AgentDeploymentTargetMetrics { 14 | id: string; 15 | } 16 | 17 | @Injectable({ 18 | providedIn: 'root', 19 | }) 20 | export class DeploymentTargetsMetricsService { 21 | private readonly deploymentTargetMetricsBaseUrl = '/api/v1/deployment-target-metrics'; 22 | private readonly httpClient = inject(HttpClient); 23 | 24 | private readonly sharedPolling$ = timer(0, 30_000).pipe( 25 | switchMap(() => this.httpClient.get(this.deploymentTargetMetricsBaseUrl)), 26 | shareReplay({ 27 | bufferSize: 1, 28 | refCount: true, 29 | }) 30 | ); 31 | 32 | poll(): Observable { 33 | return this.sharedPolling$; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/feature-flag.service.ts: -------------------------------------------------------------------------------- 1 | import {inject, Injectable} from '@angular/core'; 2 | import {map} from 'rxjs'; 3 | import {OrganizationService} from './organization.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class FeatureFlagService { 9 | private readonly organizationService = inject(OrganizationService); 10 | public isLicensingEnabled$ = this.organizationService.get().pipe(map((o) => o.features.includes('licensing'))); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/files.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | 4 | export type FileScope = 'platform' | 'organization'; 5 | 6 | @Injectable({providedIn: 'root'}) 7 | export class FilesService { 8 | private readonly httpClient = inject(HttpClient); 9 | private readonly baseUrl = '/api/v1/files'; 10 | 11 | public uploadFile(file: FormData, scope?: FileScope) { 12 | return this.httpClient.post(`${this.baseUrl}${!!scope ? '?scope=' + scope : ''}`, file); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/interfaces.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs'; 2 | 3 | export interface CrudService { 4 | list(): Observable; 5 | //get(id: string): Observable; 6 | create(request: Request): Observable; 7 | update(request: Request): Observable; 8 | delete(request: Request): Observable; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/markdown-options.factory.ts: -------------------------------------------------------------------------------- 1 | import {MarkedOptions, MarkedRenderer} from 'ngx-markdown'; 2 | import {parseInline} from 'marked'; 3 | import {captureException} from '@sentry/angular'; 4 | 5 | export function markedOptionsFactory(): MarkedOptions { 6 | const renderer = new MarkedRenderer(); 7 | const opts: MarkedOptions = {renderer: renderer}; 8 | 9 | renderer.link = ({href, text}) => { 10 | try { 11 | text = text === href ? text : parseInline(text, {...opts, async: false}); 12 | } catch (e) { 13 | captureException(e); 14 | } 15 | return `${text}`; 16 | }; 17 | 18 | return opts; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/organization-branding.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {Injectable} from '@angular/core'; 3 | import {Observable, of, tap} from 'rxjs'; 4 | import {OrganizationBranding} from '@glasskube/distr-sdk'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class OrganizationBrandingService { 10 | private readonly organizationBrandingUrl = '/api/v1/organization/branding'; 11 | private cache?: OrganizationBranding; 12 | 13 | constructor(private readonly httpClient: HttpClient) {} 14 | 15 | get(): Observable { 16 | if (this.cache) { 17 | return of(this.cache); 18 | } 19 | return this.httpClient 20 | .get(this.organizationBrandingUrl) 21 | .pipe(tap((branding) => (this.cache = branding))); 22 | } 23 | 24 | create(organizationBranding: FormData): Observable { 25 | return this.httpClient 26 | .post(this.organizationBrandingUrl, organizationBranding) 27 | .pipe(tap((obj) => (this.cache = obj))); 28 | } 29 | 30 | update(organizationBranding: FormData): Observable { 31 | return this.httpClient 32 | .put(this.organizationBrandingUrl, organizationBranding) 33 | .pipe(tap((obj) => (this.cache = obj))); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/settings.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {inject, Injectable} from '@angular/core'; 3 | import {Observable} from 'rxjs'; 4 | import {UserAccount} from '@glasskube/distr-sdk'; 5 | 6 | @Injectable({providedIn: 'root'}) 7 | export class SettingsService { 8 | private readonly httpClient = inject(HttpClient); 9 | private readonly baseUrl = '/api/v1/settings'; 10 | 11 | public updateUserSettings(request: { 12 | name?: string; 13 | password?: string; 14 | emailVerified?: boolean; 15 | }): Observable { 16 | return this.httpClient.post(`${this.baseUrl}/user`, request); 17 | } 18 | 19 | public requestEmailVerification() { 20 | return this.httpClient.post(`${this.baseUrl}/verify/request`, undefined); 21 | } 22 | 23 | public confirmEmailVerification() { 24 | return this.httpClient.post(`${this.baseUrl}/verify/confirm`, undefined); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/sidebar.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, signal} from '@angular/core'; 2 | 3 | @Injectable({providedIn: 'root'}) 4 | export class SidebarService { 5 | private showSidebarInternal = signal(false); 6 | public showSidebar = this.showSidebarInternal.asReadonly(); 7 | 8 | public toggle(): void { 9 | this.showSidebarInternal.set(!this.showSidebarInternal()); 10 | } 11 | 12 | public show(): void { 13 | this.showSidebarInternal.set(true); 14 | } 15 | 16 | public hide(): void { 17 | this.showSidebarInternal.set(false); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/ui/src/app/services/toast.service.ts: -------------------------------------------------------------------------------- 1 | import {inject, Injectable} from '@angular/core'; 2 | import {IndividualConfig, ToastrService} from 'ngx-toastr'; 3 | import {ToastComponent} from '../components/toast.component'; 4 | 5 | const toastBaseConfig: Partial = { 6 | toastComponent: ToastComponent, 7 | disableTimeOut: true, 8 | tapToDismiss: false, 9 | titleClass: '', 10 | messageClass: '', 11 | toastClass: '', 12 | positionClass: 'toast-bottom-right', 13 | }; 14 | 15 | export type ToastType = 'success' | 'error'; 16 | 17 | @Injectable({providedIn: 'root'}) 18 | export class ToastService { 19 | private readonly toastr = inject(ToastrService); 20 | 21 | public success(message: string) { 22 | this.toastr.show('', message, { 23 | ...toastBaseConfig, 24 | payload: 'success', 25 | disableTimeOut: false, 26 | }); 27 | } 28 | 29 | public error(message: string) { 30 | this.toastr.show('', message, { 31 | ...toastBaseConfig, 32 | payload: 'error', 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/ui/src/app/tutorials/stepper/tutorial-stepper.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    5 | @for (step of steps; track i; let i = $index; let last = $last) { 6 |
  1. 12 | 13 |

    {{ step.label }}

    14 |
  2. 15 | 16 | @if (!last) { 17 | 18 | } 19 | } 20 |
21 | 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /frontend/ui/src/app/tutorials/stepper/tutorial-stepper.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {CdkStepper, CdkStepperModule} from '@angular/cdk/stepper'; 3 | import {ReactiveFormsModule} from '@angular/forms'; 4 | import {FaIconComponent} from '@fortawesome/angular-fontawesome'; 5 | import {NgTemplateOutlet} from '@angular/common'; 6 | import {faCircle, faCircleCheck} from '@fortawesome/free-regular-svg-icons'; 7 | 8 | @Component({ 9 | selector: 'app-tutorial-stepper', 10 | templateUrl: './tutorial-stepper.component.html', 11 | providers: [{provide: CdkStepper, useExisting: TutorialStepperComponent}], 12 | imports: [CdkStepperModule, ReactiveFormsModule, FaIconComponent, NgTemplateOutlet], 13 | }) 14 | export class TutorialStepperComponent extends CdkStepper { 15 | protected readonly faCircle = faCircle; 16 | protected readonly faCircleCheck = faCircleCheck; 17 | 18 | protected isCurrentStep(i: number): boolean { 19 | return this.selectedIndex === i; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/ui/src/app/tutorials/tutorials.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, inject} from '@angular/core'; 2 | import {ReactiveFormsModule} from '@angular/forms'; 3 | import {RouterLink} from '@angular/router'; 4 | import {FaIconComponent} from '@fortawesome/angular-fontawesome'; 5 | import {faArrowRight, faCheck, faLightbulb} from '@fortawesome/free-solid-svg-icons'; 6 | import {TutorialsService} from '../services/tutorials.service'; 7 | import {AsyncPipe} from '@angular/common'; 8 | import {faCircle, faCircleCheck} from '@fortawesome/free-regular-svg-icons'; 9 | 10 | @Component({ 11 | selector: 'app-tutorials', 12 | imports: [ReactiveFormsModule, FaIconComponent, RouterLink, AsyncPipe], 13 | templateUrl: './tutorials.component.html', 14 | }) 15 | export class TutorialsComponent { 16 | protected readonly faLightbulb = faLightbulb; 17 | protected readonly faArrowRight = faArrowRight; 18 | protected readonly faCheck = faCheck; 19 | protected readonly tutorialsService = inject(TutorialsService); 20 | 21 | ngOnInit() { 22 | this.tutorialsService.refreshList(); 23 | } 24 | 25 | protected readonly faCircle = faCircle; 26 | protected readonly faCircleCheck = faCircleCheck; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/ui/src/app/tutorials/utils.ts: -------------------------------------------------------------------------------- 1 | import {TutorialProgress, TutorialProgressEvent} from '../types/tutorials'; 2 | 3 | export function getExistingTask( 4 | progress: TutorialProgress | undefined, 5 | stepId: string, 6 | taskId: string 7 | ): TutorialProgressEvent | undefined { 8 | return findTask(progress?.events ?? [], stepId, taskId); 9 | } 10 | 11 | export function getLastExistingTask( 12 | progress: TutorialProgress | undefined, 13 | stepId: string, 14 | taskId: string 15 | ): TutorialProgressEvent | undefined { 16 | return findTask((progress?.events ?? []).concat().reverse(), stepId, taskId); 17 | } 18 | 19 | function findTask(events: TutorialProgressEvent[], stepId: string, taskId: string): TutorialProgressEvent | undefined { 20 | return events.find((e) => e.stepId === stepId && e.taskId === taskId); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/ui/src/app/types/application-license.ts: -------------------------------------------------------------------------------- 1 | import {Application, ApplicationVersion, BaseModel, Named, UserAccount} from '@glasskube/distr-sdk'; 2 | 3 | export interface ApplicationLicense extends BaseModel, Named { 4 | expiresAt?: Date; 5 | applicationId?: string; 6 | application?: Application; 7 | versions?: ApplicationVersion[]; 8 | ownerUserAccountId?: string; 9 | owner?: UserAccount; 10 | 11 | registryUrl?: string; 12 | registryUsername?: string; 13 | registryPassword?: string; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/ui/src/app/types/artifact-version-pull.ts: -------------------------------------------------------------------------------- 1 | import {UserAccount} from '@glasskube/distr-sdk'; 2 | import {BaseArtifact, BaseArtifactVersion} from '../services/artifacts.service'; 3 | 4 | export interface ArtifactVersionPull { 5 | createdAt: string; 6 | remoteAddress?: string; 7 | userAccount?: UserAccount; 8 | artifact: BaseArtifact; 9 | artifactVersion: BaseArtifactVersion; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/ui/src/app/types/deployment-log-record.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel} from '@glasskube/distr-sdk'; 2 | 3 | export interface DeploymentLogRecord extends BaseModel { 4 | deploymentId: string; 5 | deploymentRevisionId: string; 6 | resource: string; 7 | timestamp: string; 8 | severity: string; 9 | body: string; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/ui/src/app/types/organization.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel, Named, UserRole} from '@glasskube/distr-sdk'; 2 | 3 | export type Feature = 'licensing'; 4 | 5 | export interface Organization extends BaseModel, Named { 6 | slug?: string; 7 | features: Feature[]; 8 | appDomain?: string; 9 | registryDomain?: string; 10 | emailFromAddress?: string; 11 | } 12 | 13 | export interface OrganizationWithUserRole extends Organization { 14 | userRole: UserRole; 15 | joinedOrgAt: string; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/ui/src/app/types/timeseries-options.ts: -------------------------------------------------------------------------------- 1 | export type TimeseriesOptions = {limit?: number; before?: Date; after?: Date}; 2 | 3 | export function timeseriesOptionsAsParams(options?: TimeseriesOptions): Record { 4 | const params: Record = {}; 5 | if (options?.limit !== undefined) { 6 | params['limit'] = options.limit.toFixed(); 7 | } 8 | if (options?.before !== undefined) { 9 | params['before'] = options.before.toISOString(); 10 | } 11 | if (options?.after !== undefined) { 12 | params['after'] = options.after.toISOString(); 13 | } 14 | return params; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/ui/src/app/types/tutorials.ts: -------------------------------------------------------------------------------- 1 | export type Tutorial = 'branding' | 'agents' | 'registry'; 2 | 3 | export interface TutorialProgressEvent { 4 | stepId: string; 5 | taskId: string; 6 | value?: any; 7 | } 8 | 9 | export interface TutorialProgressRequest extends TutorialProgressEvent { 10 | markCompleted?: boolean; 11 | } 12 | 13 | export interface TutorialProgress { 14 | tutorial: Tutorial; 15 | createdAt?: string; 16 | completedAt?: string; 17 | events?: TutorialProgressEvent[]; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/ui/src/app/verify/verify.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, inject} from '@angular/core'; 2 | import {SettingsService} from '../services/settings.service'; 3 | import {firstValueFrom} from 'rxjs'; 4 | import {ToastService} from '../services/toast.service'; 5 | import {AuthService} from '../services/auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-verify', 9 | templateUrl: './verify.component.html', 10 | imports: [], 11 | }) 12 | export class VerifyComponent { 13 | private readonly settings = inject(SettingsService); 14 | private readonly toast = inject(ToastService); 15 | private readonly auth = inject(AuthService); 16 | public requestMailEnabled = true; 17 | 18 | public async requestMail() { 19 | this.requestMailEnabled = false; 20 | try { 21 | await firstValueFrom(this.settings.requestEmailVerification()); 22 | this.toast.success('Verification email has been sent. Check your inbox.'); 23 | } catch (e) { 24 | this.requestMailEnabled = true; 25 | } 26 | } 27 | 28 | public async logoutAndRedirectToLogin() { 29 | await firstValueFrom(this.auth.logout()); 30 | location.assign('/login'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/ui/src/buildconfig/.gitignore: -------------------------------------------------------------------------------- 1 | version.json 2 | -------------------------------------------------------------------------------- /frontend/ui/src/buildconfig/index.ts: -------------------------------------------------------------------------------- 1 | import * as buildConfig from './version.json'; 2 | 3 | export {buildConfig}; 4 | -------------------------------------------------------------------------------- /frontend/ui/src/env/env.prod.ts: -------------------------------------------------------------------------------- 1 | import {Environment} from './types'; 2 | 3 | export const environment: Environment = {production: true}; 4 | -------------------------------------------------------------------------------- /frontend/ui/src/env/env.ts: -------------------------------------------------------------------------------- 1 | import {Environment} from './types'; 2 | 3 | export const environment: Environment = {production: false}; 4 | -------------------------------------------------------------------------------- /frontend/ui/src/env/remote.ts: -------------------------------------------------------------------------------- 1 | import {RemoteEnvironment} from './types'; 2 | 3 | export async function getRemoteEnvironment(): Promise { 4 | const cached = sessionStorage['remoteEnvironment']; 5 | if (cached) { 6 | try { 7 | return JSON.parse(cached); 8 | } catch (e) {} 9 | } 10 | const result = await (await fetch('/internal/environment')).json(); 11 | sessionStorage['remoteEnvironment'] = JSON.stringify(result); 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/ui/src/env/types.ts: -------------------------------------------------------------------------------- 1 | export interface Environment { 2 | production: boolean; 3 | } 4 | 5 | export interface RemoteEnvironment { 6 | readonly sentryDsn?: string; 7 | readonly sentryTraceSampleRate?: number; 8 | readonly posthogToken?: string; 9 | readonly posthogApiHost?: string; 10 | readonly posthogUiHost?: string; 11 | readonly registryHost: string; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Distr 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/ui/src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8080", 4 | "secure": false 5 | }, 6 | "/internal": { 7 | "target": "http://localhost:8080", 8 | "secure": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/ui/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @use 'tailwindcss'; 3 | @use 'styles/theme.scss'; 4 | @use 'styles/markdown.scss'; 5 | 6 | @import '@fontsource/inter/latin-300.css'; 7 | @import '@fontsource/inter/latin-400.css'; 8 | @import '@fontsource/inter/latin-600.css'; 9 | @import '@fontsource/inter/latin-700.css'; 10 | @import '@fontsource/poppins/latin-600.css'; 11 | @import '@angular/cdk/overlay-prebuilt.css'; 12 | @import '../../../node_modules/ngx-toastr/toastr.css'; 13 | -------------------------------------------------------------------------------- /frontend/ui/src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | @theme { 2 | --spacing-108: 27rem; 3 | --spacing-128: 32rem; 4 | --spacing-144: 36rem; 5 | --spacing-160: 40rem; 6 | --spacing-180: 45rem; 7 | --spacing-200: 50rem; 8 | --spacing-256: 64rem; 9 | 10 | --color-primary-50: #eff6ff; 11 | --color-primary-100: #dbeafe; 12 | --color-primary-200: #bfdbfe; 13 | --color-primary-300: #93c5fd; 14 | --color-primary-400: #60a5fa; 15 | --color-primary-500: #3b82f6; 16 | --color-primary-600: #2563eb; 17 | --color-primary-700: #1d4ed8; 18 | --color-primary-800: #1e40af; 19 | --color-primary-900: #1e3a8a; 20 | --color-primary-950: #172554; 21 | 22 | --font-sans: 'Inter', 'system-ui', 'sans-serif'; 23 | --font-display: 'Poppins', 'Inter', 'system-ui', 'sans-serif'; 24 | --font-mono: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; 25 | 26 | --breakpoint-3xl: 112rem; 27 | --breakpoint-4xl: 128rem; 28 | --breakpoint-5xl: 144rem; 29 | } 30 | 31 | @custom-variant dark (&:where(.dark, .dark *)); 32 | 33 | @plugin 'flowbite/plugin'; 34 | @source '../../../node_modules/flowbite'; 35 | 36 | @layer base { 37 | button:not(:disabled), 38 | [role='button']:not(:disabled) { 39 | cursor: pointer; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/ui/src/util/arrays.ts: -------------------------------------------------------------------------------- 1 | export type Predicate = (arg: T) => R; 2 | 3 | export function distinctBy(predicate: Predicate): (input: T[]) => T[] { 4 | return (input) => 5 | input.filter((value: T, index, self) => { 6 | return self.findIndex((element) => predicate(element) === predicate(value)) === index; 7 | }); 8 | } 9 | 10 | export function compareBy(predicate: Predicate): (a: T, b: T) => number { 11 | return (a, b) => predicate(a).localeCompare(predicate(b)); 12 | } 13 | 14 | export function maxBy( 15 | input: T[], 16 | predicate: Predicate, 17 | cmp: (a: E, b: E) => boolean = (a, b) => a > b 18 | ): T | undefined { 19 | let max: T | undefined; 20 | let maxp: E | undefined; 21 | for (const el of input) { 22 | const elp = predicate(el); 23 | if (maxp === undefined || cmp(elp, maxp)) { 24 | max = el; 25 | maxp = elp; 26 | } 27 | } 28 | return max; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/ui/src/util/blob.ts: -------------------------------------------------------------------------------- 1 | export function base64ToBlob(base64String: string, contentType = ''): Blob { 2 | const byteCharacters = atob(base64String); 3 | const byteArrays = []; 4 | 5 | for (let i = 0; i < byteCharacters.length; i++) { 6 | byteArrays.push(byteCharacters.charCodeAt(i)); 7 | } 8 | 9 | const byteArray = new Uint8Array(byteArrays); 10 | return new Blob([byteArray], {type: contentType}); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/ui/src/util/dates.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import dayjs from 'dayjs'; 3 | import {Duration} from 'dayjs/plugin/duration'; 4 | 5 | export function isOlderThan(date: dayjs.ConfigType, duration: Duration): boolean { 6 | return dayjs.duration(Math.abs(dayjs(date).diff(dayjs()))) > duration; 7 | } 8 | 9 | @Pipe({name: 'relativeDate'}) 10 | export class RelativeDatePipe implements PipeTransform { 11 | transform(value: dayjs.ConfigType, withoutSuffix: boolean = false): string { 12 | const d = dayjs(value); 13 | if (d.isBefore()) { 14 | return dayjs(value).fromNow(withoutSuffix); 15 | } else { 16 | return dayjs(value).toNow(withoutSuffix); 17 | } 18 | } 19 | } 20 | 21 | export function isExpired(obj: {expiresAt?: Date | string}): boolean { 22 | return obj.expiresAt ? dayjs(obj.expiresAt).isBefore() : false; 23 | } 24 | 25 | export function isArchived(obj: {archivedAt?: Date | string}): boolean { 26 | return obj.archivedAt ? dayjs(obj.archivedAt).isBefore() : false; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/ui/src/util/errors.ts: -------------------------------------------------------------------------------- 1 | import {HttpErrorResponse} from '@angular/common/http'; 2 | 3 | export function displayedInToast(err: any): boolean { 4 | if (err instanceof HttpErrorResponse) { 5 | return !err.status || err.status === 429 || err.status >= 500; 6 | } 7 | return false; 8 | } 9 | 10 | export function getFormDisplayedError(err: any): string | undefined { 11 | if (!displayedInToast(err)) { 12 | if (err instanceof HttpErrorResponse && typeof err.error === 'string') { 13 | return err.error; 14 | } else { 15 | return 'Something went wrong'; 16 | } 17 | } 18 | return; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/ui/src/util/filter.ts: -------------------------------------------------------------------------------- 1 | import {combineLatest, map, Observable, startWith} from 'rxjs'; 2 | import {FormControl} from '@angular/forms'; 3 | 4 | export function filteredByFormControl( 5 | dataSource: Observable, 6 | formControl: FormControl, 7 | matchFn: (item: T, search: string) => boolean 8 | ): Observable { 9 | return combineLatest([dataSource, formControl.valueChanges.pipe(startWith(''))]).pipe( 10 | map(([items, search]) => { 11 | return items.filter((it) => matchFn(it, search)); 12 | }) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/ui/src/util/forms.ts: -------------------------------------------------------------------------------- 1 | import {FormGroup} from '@angular/forms'; 2 | 3 | export function enableControlsWithoutEvent(formGroup: FormGroup) { 4 | toggleControlsWithoutEvent(formGroup, true); 5 | } 6 | 7 | export function disableControlsWithoutEvent(formGroup: FormGroup) { 8 | toggleControlsWithoutEvent(formGroup, false); 9 | } 10 | 11 | export function toggleControlsWithoutEvent(formGroup: FormGroup, enabled: boolean) { 12 | if (enabled) { 13 | formGroup.enable({emitEvent: false}); 14 | } else { 15 | formGroup.disable({emitEvent: false}); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/ui/src/util/model.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import {isOlderThan} from './dates'; 3 | import {Pipe, PipeTransform} from '@angular/core'; 4 | import {Duration} from 'dayjs/plugin/duration'; 5 | import {BaseModel} from '@glasskube/distr-sdk'; 6 | 7 | export function isStale(model: BaseModel, duration: Duration = dayjs.duration({seconds: 60})): boolean { 8 | return isOlderThan(model.createdAt, duration); 9 | } 10 | 11 | @Pipe({name: 'isStale'}) 12 | export class IsStalePipe implements PipeTransform { 13 | transform(value: BaseModel): boolean { 14 | return isStale(value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/ui/src/util/secureImage.ts: -------------------------------------------------------------------------------- 1 | import {inject, Pipe, PipeTransform} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {DomSanitizer, SafeUrl} from '@angular/platform-browser'; 4 | import {map, Observable, of} from 'rxjs'; 5 | 6 | @Pipe({ 7 | name: 'secureImage', 8 | }) 9 | export class SecureImagePipe implements PipeTransform { 10 | private readonly httpClient = inject(HttpClient); 11 | 12 | transform(url?: string): Observable { 13 | if (!url || !url.length) { 14 | return of('/distr-logo.svg'); 15 | } 16 | return this.httpClient.get(url, {responseType: 'blob'}).pipe(map((val) => URL.createObjectURL(val))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/ui/src/util/slug.ts: -------------------------------------------------------------------------------- 1 | export const slugPattern = /^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*$/; 2 | export const slugMaxLength = 64; 3 | -------------------------------------------------------------------------------- /frontend/ui/src/util/units.spec.ts: -------------------------------------------------------------------------------- 1 | import {formatBytes} from './units'; 2 | 3 | describe('formatBytes', () => { 4 | it('should format 1000 to 1,000B', () => expect(formatBytes(1000, 'en-US')).toEqual('1,000B')); 5 | it('should format 1200 to 1.172KiB', () => expect(formatBytes(1200, 'en-US')).toEqual('1.172KiB')); 6 | it('should format -1024 to -1KiB', () => expect(formatBytes(-1024, 'en-US')).toEqual('-1KiB')); 7 | it('should format 8734568 to 8.330KiB', () => expect(formatBytes(1200, 'en-US')).toEqual('1.172KiB')); 8 | it('should format 1.5TiB to 1,536GiB', () => expect(formatBytes(1649267441664, 'en-US')).toEqual('1,536GiB')); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/ui/src/util/units.ts: -------------------------------------------------------------------------------- 1 | import {formatNumber} from '@angular/common'; 2 | import {inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; 3 | 4 | const prefixes = ['', 'Ki', 'Mi', 'Gi']; 5 | 6 | export function formatBytes(input: number, locale: string, digitsInfo?: string) { 7 | const index = Math.min(prefixes.length - 1, Math.floor(Math.log2(Math.abs(input)) / 10)); 8 | return formatNumber(input / Math.pow(1024, index), locale, digitsInfo) + prefixes[index] + 'B'; 9 | } 10 | 11 | @Pipe({name: 'bytes'}) 12 | export class BytesPipe implements PipeTransform { 13 | private readonly locale = inject(LOCALE_ID); 14 | 15 | transform(value: number, digitsInfo?: string) { 16 | return formatBytes(value, this.locale, digitsInfo); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/ui/src/util/validation.ts: -------------------------------------------------------------------------------- 1 | export const KUBERNETES_RESOURCE_MAX_LENGTH = 253; 2 | export const KUBERNETES_RESOURCE_NAME_REGEX = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/; 3 | export const HELM_RELEASE_NAME_REGEX = /^[a-z0-9]([-a-z0-9]*)?[a-z0-9]$/; 4 | export const HELM_RELEASE_NAME_MAX_LENGTH = 53; 5 | -------------------------------------------------------------------------------- /frontend/ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/ui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /hack/sentry-release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/env sh 2 | 3 | if [ -z "$SENTRY_AUTH_TOKEN" ]; then 4 | echo "SENTRY_AUTH_TOKEN is not set" 5 | exit 1 6 | fi 7 | 8 | if [ -z "$VERSION" ]; then 9 | echo "VERSION is not set" 10 | exit 1 11 | fi 12 | 13 | export SENTRY_ORG="glasskube" 14 | export SENTRY_PROJECT="distr-frontend" 15 | 16 | npx sentry-cli releases new "$VERSION" 17 | npx sentry-cli releases set-commits "$VERSION" --auto 18 | npx sentry-cli sourcemaps upload --release="$VERSION" internal/frontend/dist/ui/browser 19 | npx sentry-cli releases finalize "$VERSION" 20 | -------------------------------------------------------------------------------- /hack/update-frontend-version.js: -------------------------------------------------------------------------------- 1 | import {execSync} from 'node:child_process'; 2 | import {writeFile} from 'node:fs/promises'; 3 | import {env} from 'node:process'; 4 | 5 | let version = env['VERSION']; 6 | if (!version) { 7 | const tag = execSync('git tag --points-at HEAD').toString().trim(); 8 | if (!tag) { 9 | version = 'snapshot'; 10 | } else { 11 | version = tag; 12 | } 13 | } 14 | 15 | let commit = env['COMMIT']; 16 | if (!commit) { 17 | commit = execSync('git rev-parse --short HEAD').toString().trim(); 18 | } 19 | 20 | const buildconfig = {version, commit, release: version !== 'snapshot'}; 21 | 22 | console.log(buildconfig); 23 | 24 | await writeFile('frontend/ui/src/buildconfig/version.json', JSON.stringify(buildconfig, null, 2)); 25 | -------------------------------------------------------------------------------- /internal/agentclient/httpstatus.go: -------------------------------------------------------------------------------- 1 | package agentclient 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | var ErrHttpStatus = errors.New("non-ok http status") 12 | 13 | func checkStatus(r *http.Response, err error) (*http.Response, error) { 14 | if err != nil || statusOK(r) { 15 | return r, err 16 | } else { 17 | if errorBody, err := io.ReadAll(r.Body); err == nil { 18 | return r, fmt.Errorf("%w: %v (%v)", ErrHttpStatus, r.Status, strings.TrimSpace(string(errorBody))) 19 | } 20 | return r, fmt.Errorf("%w: %v", ErrHttpStatus, r.Status) 21 | } 22 | } 23 | 24 | func statusOK(r *http.Response) bool { 25 | return 200 <= r.StatusCode && r.StatusCode < 300 26 | } 27 | -------------------------------------------------------------------------------- /internal/agentclient/useragent/user_agent.go: -------------------------------------------------------------------------------- 1 | package useragent 2 | 3 | const DistrAgentUserAgent = "DistrAgentClient" 4 | -------------------------------------------------------------------------------- /internal/agentenv/env.go: -------------------------------------------------------------------------------- 1 | package agentenv 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/glasskube/distr/internal/envparse" 8 | "github.com/glasskube/distr/internal/envutil" 9 | ) 10 | 11 | var ( 12 | AgentVersionID = envutil.GetEnv("DISTR_AGENT_VERSION_ID") 13 | Interval = envutil.GetEnvParsedOrDefault("DISTR_INTERVAL", envparse.PositiveDuration, 5*time.Second) 14 | DistrRegistryHost = envutil.GetEnv("DISTR_REGISTRY_HOST") 15 | DistrRegistryPlainHTTP = envutil.GetEnvParsedOrDefault("DISTR_REGISTRY_PLAIN_HTTP", strconv.ParseBool, false) 16 | ) 17 | -------------------------------------------------------------------------------- /internal/agentlogs/exporter.go: -------------------------------------------------------------------------------- 1 | package agentlogs 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | 7 | "github.com/glasskube/distr/api" 8 | "go.uber.org/multierr" 9 | ) 10 | 11 | type Exporter interface { 12 | Logs(ctx context.Context, logs []api.DeploymentLogRecord) error 13 | } 14 | 15 | type chunkExporter struct { 16 | delegate Exporter 17 | chunkSize int 18 | } 19 | 20 | // ChunkExporter returns an exporter that delegates to the given exporter but sends log records in batches with the 21 | // designated batchSize. 22 | func ChunkExporter(exporter Exporter, chunkSize int) Exporter { 23 | return &chunkExporter{chunkSize: chunkSize, delegate: exporter} 24 | } 25 | 26 | func (be *chunkExporter) Logs(ctx context.Context, logs []api.DeploymentLogRecord) (err error) { 27 | if len(logs) == 0 { 28 | return 29 | } 30 | for logs := range slices.Chunk(logs, be.chunkSize) { 31 | multierr.AppendInto(&err, be.delegate.Logs(ctx, logs)) 32 | } 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /internal/agentlogs/record.go: -------------------------------------------------------------------------------- 1 | package agentlogs 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/glasskube/distr/api" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type LogRecordOption func(lr *api.DeploymentLogRecord) 12 | 13 | func WithSeverity(severity string) LogRecordOption { 14 | return func(lr *api.DeploymentLogRecord) { 15 | lr.Severity = severity 16 | } 17 | } 18 | 19 | func WithTimestamp(ts time.Time) LogRecordOption { 20 | return func(lr *api.DeploymentLogRecord) { 21 | lr.Timestamp = ts 22 | } 23 | } 24 | 25 | func NewRecord(deploymentID, deploymentRevisionID uuid.UUID, resource, severity, body string) api.DeploymentLogRecord { 26 | record := api.DeploymentLogRecord{ 27 | DeploymentID: deploymentID, 28 | DeploymentRevisionID: deploymentRevisionID, 29 | Resource: resource, 30 | Timestamp: time.Now(), 31 | Severity: severity, 32 | Body: body, 33 | } 34 | messageParts := strings.SplitN(body, " ", 2) 35 | if len(messageParts) > 1 { 36 | if ts, err := time.Parse(time.RFC3339Nano, messageParts[0]); err == nil { 37 | record.Timestamp = ts 38 | record.Body = strings.TrimSpace(messageParts[1]) 39 | } 40 | } 41 | return record 42 | } 43 | -------------------------------------------------------------------------------- /internal/apierrors/errors.go: -------------------------------------------------------------------------------- 1 | package apierrors 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotFound = errors.New("not found") 7 | ErrAlreadyExists = errors.New("already exists") 8 | ErrConflict = errors.New("conflict") 9 | ErrForbidden = errors.New("forbidden") 10 | ErrQuotaExceeded = errors.New("quota exceeded") 11 | ) 12 | -------------------------------------------------------------------------------- /internal/authkey/key.go: -------------------------------------------------------------------------------- 1 | package authkey 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | const keyPrefix = "distr-" 13 | 14 | type Key [16]byte 15 | 16 | var ErrInvalidAccessKey = errors.New("invalid access key") 17 | 18 | func Parse(encoded string) (Key, error) { 19 | if !strings.HasPrefix(encoded, keyPrefix) { 20 | return Key{}, ErrInvalidAccessKey 21 | } else if decoded, err := hex.DecodeString(strings.TrimPrefix(encoded, keyPrefix)); err != nil { 22 | return Key{}, fmt.Errorf("%w: %w", ErrInvalidAccessKey, err) 23 | } else { 24 | return Key(decoded), nil 25 | } 26 | } 27 | 28 | func NewKey() (key Key, err error) { 29 | _, err = rand.Read(key[:]) 30 | return 31 | } 32 | 33 | func (key Key) Serialize() string { return keyPrefix + hex.EncodeToString(key[:]) } 34 | 35 | func (key Key) MarshalJSON() ([]byte, error) { return json.Marshal(key.Serialize()) } 36 | 37 | func (key *Key) Scan(src any) error { 38 | switch v := src.(type) { 39 | case []byte: 40 | if len(v) == 16 { 41 | *key = Key(v[:]) 42 | return nil 43 | } 44 | } 45 | return errors.New("cannot scan into Key") 46 | } 47 | -------------------------------------------------------------------------------- /internal/authn/authinfo/authinfo.go: -------------------------------------------------------------------------------- 1 | package authinfo 2 | 3 | import ( 4 | "github.com/glasskube/distr/internal/types" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type AuthInfo interface { 9 | CurrentUserID() uuid.UUID 10 | CurrentUserEmail() string 11 | CurrentUserRole() *types.UserRole 12 | CurrentOrgID() *uuid.UUID 13 | CurrentUserEmailVerified() bool 14 | Token() any 15 | } 16 | 17 | type AgentAuthInfo interface { 18 | CurrentDeploymentTargetID() uuid.UUID 19 | CurrentOrgID() uuid.UUID 20 | Token() any 21 | } 22 | -------------------------------------------------------------------------------- /internal/authn/authinfo/token.go: -------------------------------------------------------------------------------- 1 | package authinfo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/glasskube/distr/internal/apierrors" 9 | "github.com/glasskube/distr/internal/authkey" 10 | "github.com/glasskube/distr/internal/authn" 11 | "github.com/glasskube/distr/internal/db" 12 | ) 13 | 14 | func FromAuthKey(ctx context.Context, token authkey.Key) (AuthInfo, error) { 15 | if at, err := db.GetAccessTokenByKeyUpdatingLastUsed(ctx, token); err != nil { 16 | if errors.Is(err, apierrors.ErrNotFound) { 17 | err = fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err) 18 | } 19 | return nil, err 20 | } else { 21 | return &SimpleAuthInfo{ 22 | userID: at.UserAccount.ID, 23 | userEmail: at.UserAccount.Email, 24 | emailVerified: at.UserAccount.EmailVerifiedAt != nil, 25 | organizationID: &at.OrganizationID, 26 | userRole: &at.UserRole, 27 | rawToken: token, 28 | }, nil 29 | } 30 | } 31 | 32 | func AuthKeyAuthenticator() authn.Authenticator[authkey.Key, AuthInfo] { 33 | return authn.AuthenticatorFunc[authkey.Key, AuthInfo]( 34 | func(ctx context.Context, key authkey.Key) (AuthInfo, error) { 35 | return FromAuthKey(ctx, key) 36 | }, 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /internal/authn/authkey/provider.go: -------------------------------------------------------------------------------- 1 | package authkey 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/glasskube/distr/internal/authkey" 8 | "github.com/glasskube/distr/internal/authn" 9 | ) 10 | 11 | func Authenticator() authn.Authenticator[string, authkey.Key] { 12 | return authn.AuthenticatorFunc[string, authkey.Key]( 13 | func(ctx context.Context, token string) (authkey.Key, error) { 14 | if key, err := authkey.Parse(token); err != nil { 15 | return authkey.Key{}, fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err) 16 | } else { 17 | return key, nil 18 | } 19 | }, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /internal/authn/errors.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | // ErrNoAuthentication implies that the provider *did not* find relevant 9 | // authentication information on the Request 10 | var ErrNoAuthentication = errors.New("not authenticated") 11 | 12 | // ErrBadAuthentication implies that the provider *did* find relevant 13 | // authentication information on the Request but it is not valid 14 | var ErrBadAuthentication = errors.New("bad authentication") 15 | 16 | type HttpHeaderError struct { 17 | wrapped error 18 | headers http.Header 19 | } 20 | 21 | // ResponseHEaders implements WithResponseHeaders. 22 | func (err *HttpHeaderError) ResponseHeaders() http.Header { 23 | return err.headers 24 | } 25 | 26 | var _ WithResponseHeaders = &HttpHeaderError{} 27 | 28 | func NewHttpHeaderError(err error, headers http.Header) error { 29 | return &HttpHeaderError{ 30 | wrapped: err, 31 | headers: headers, 32 | } 33 | } 34 | 35 | func (err *HttpHeaderError) Error() string { 36 | return err.wrapped.Error() 37 | } 38 | 39 | func (err *HttpHeaderError) Unwrap() error { 40 | return err.wrapped 41 | } 42 | -------------------------------------------------------------------------------- /internal/authn/jwt/provider.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/glasskube/distr/internal/authn" 8 | "github.com/go-chi/jwtauth/v5" 9 | "github.com/lestrrat-go/jwx/v2/jwt" 10 | ) 11 | 12 | func Authenticator(jwtAuthGetter func() *jwtauth.JWTAuth) authn.Authenticator[string, jwt.Token] { 13 | return authn.AuthenticatorFunc[string, jwt.Token]( 14 | func(ctx context.Context, s string) (jwt.Token, error) { 15 | if token, err := jwtauth.VerifyToken(jwtAuthGetter(), s); err != nil { 16 | return nil, fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err) 17 | } else { 18 | return token, nil 19 | } 20 | }, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /internal/authn/response.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type WithResponseHeaders interface { 8 | ResponseHeaders() http.Header 9 | } 10 | -------------------------------------------------------------------------------- /internal/buildconfig/buildconfig.go: -------------------------------------------------------------------------------- 1 | package buildconfig 2 | 3 | const ( 4 | snapshot = "snapshot" 5 | ) 6 | 7 | var ( 8 | version = snapshot 9 | commit string 10 | ) 11 | 12 | func Version() string { 13 | return version 14 | } 15 | 16 | func Commit() string { 17 | return commit 18 | } 19 | 20 | func IsRelease() bool { 21 | return !IsDevelopment() 22 | } 23 | 24 | func IsDevelopment() bool { 25 | return version == snapshot 26 | } 27 | -------------------------------------------------------------------------------- /internal/cleanup/deployment_log_records.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | import ( 4 | "context" 5 | 6 | internalctx "github.com/glasskube/distr/internal/context" 7 | "github.com/glasskube/distr/internal/db" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | func RunDeploymentLogRecordCleanup(ctx context.Context) error { 12 | log := internalctx.GetLogger(ctx) 13 | if count, err := db.CleanupDeploymentLogRecords(ctx); err != nil { 14 | return err 15 | } else { 16 | log.Info("DeploymentLogRecord cleanup finished", zap.Int64("rowsDeleted", count)) 17 | return nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/cleanup/deployment_revision_status.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | import ( 4 | "context" 5 | 6 | internalctx "github.com/glasskube/distr/internal/context" 7 | "github.com/glasskube/distr/internal/db" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | func RunDeploymentRevisionStatusCleanup(ctx context.Context) error { 12 | log := internalctx.GetLogger(ctx) 13 | if count, err := db.CleanupDeploymentRevisionStatus(ctx); err != nil { 14 | return err 15 | } else { 16 | log.Info("DeploymentRevisionStatus cleanup finished", zap.Int64("rowsDeleted", count)) 17 | return nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/cleanup/deployment_target_metrics.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | import ( 4 | "context" 5 | 6 | internalctx "github.com/glasskube/distr/internal/context" 7 | "github.com/glasskube/distr/internal/db" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | func RunDeploymentTargetMetricsCleanup(ctx context.Context) error { 12 | log := internalctx.GetLogger(ctx) 13 | if count, err := db.CleanupDeploymentTargetMetrics(ctx); err != nil { 14 | return err 15 | } else { 16 | log.Info("DeploymentTargetMetrics cleanup finished", zap.Int64("rowsDeleted", count)) 17 | return nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/cleanup/deployment_target_status.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | import ( 4 | "context" 5 | 6 | internalctx "github.com/glasskube/distr/internal/context" 7 | "github.com/glasskube/distr/internal/db" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | func RunDeploymentTargetStatusCleanup(ctx context.Context) error { 12 | log := internalctx.GetLogger(ctx) 13 | if count, err := db.CleanupDeploymentTargetStatus(ctx); err != nil { 14 | return err 15 | } else { 16 | log.Info("DeploymentTargetStatus cleanup finished", zap.Int64("rowsDeleted", count)) 17 | return nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/contenttype/contenttype.go: -------------------------------------------------------------------------------- 1 | package contenttype 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type ContentType struct { 9 | MediaType string 10 | Charset string 11 | Boundary string 12 | } 13 | 14 | func ParseContentType(value string) (*ContentType, error) { 15 | directives := strings.Split(value, ";") 16 | result := ContentType{ 17 | MediaType: strings.TrimSpace(directives[0]), 18 | } 19 | if len(directives) > 1 { 20 | for _, directive := range directives[1:] { 21 | parts := strings.SplitN(strings.TrimSpace(directive), "=", 2) 22 | if len(parts) == 2 { 23 | switch parts[0] { 24 | case "charset": 25 | result.Charset = parts[1] 26 | case "boundary": 27 | result.Boundary = parts[1] 28 | default: 29 | return nil, fmt.Errorf("content type has unknown directive %v", directive) 30 | } 31 | } else { 32 | return nil, fmt.Errorf("content type has invalid directive %v", directive) 33 | } 34 | } 35 | } 36 | return &result, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/contenttype/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Adaapted from https://github.com/glasskube/glasskube/tree/main/internal/contenttype 3 | */ 4 | package contenttype 5 | -------------------------------------------------------------------------------- /internal/contenttype/mediatype.go: -------------------------------------------------------------------------------- 1 | package contenttype 2 | 3 | import ( 4 | "fmt" 5 | "net/textproto" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | MediaTypeJSON = "application/json" 11 | MediaTypeYAML = "application/yaml" 12 | MediaTypeOctetStream = "application/octet-stream" 13 | MediaTypeTextYAML = "text/yaml" 14 | MediaTypeTextPlain = "text/plain" 15 | MediaTypeTextXYaml = "text/x-yaml" 16 | MediaTypeApplicationXYaml = "application/x-yaml" 17 | ) 18 | 19 | func IsYaml(header textproto.MIMEHeader) error { 20 | return HasMediaType(header, 21 | MediaTypeJSON, 22 | MediaTypeYAML, 23 | MediaTypeTextYAML, 24 | MediaTypeTextXYaml, 25 | MediaTypeApplicationXYaml) 26 | } 27 | 28 | func HasMediaType(header textproto.MIMEHeader, acceptedContentTypes ...string) error { 29 | contentType, err := ParseContentType(header.Get("Content-Type")) 30 | if err != nil { 31 | return err 32 | } 33 | if contentType.MediaType == "" { 34 | return nil 35 | } 36 | for _, t := range acceptedContentTypes { 37 | if contentType.MediaType == t { 38 | return nil 39 | } 40 | } 41 | return fmt.Errorf("unacceptable media type: %v (acceptable media types are %v)", 42 | contentType.MediaType, strings.Join(acceptedContentTypes, ", ")) 43 | } 44 | -------------------------------------------------------------------------------- /internal/customdomains/customdomains.go: -------------------------------------------------------------------------------- 1 | package customdomains 2 | 3 | import ( 4 | "net/mail" 5 | "regexp" 6 | 7 | "github.com/glasskube/distr/internal/env" 8 | "github.com/glasskube/distr/internal/types" 9 | "github.com/glasskube/distr/internal/util" 10 | ) 11 | 12 | var urlSchemeRegex = regexp.MustCompile("^https?://") 13 | 14 | func AppDomainOrDefault(o types.Organization) string { 15 | if o.AppDomain != nil { 16 | d := *o.AppDomain 17 | if urlSchemeRegex.MatchString(d) { 18 | return d 19 | } else { 20 | scheme := urlSchemeRegex.FindString(env.Host()) 21 | if scheme == "" { 22 | scheme = "https://" 23 | } 24 | return scheme + d 25 | } 26 | } else { 27 | return env.Host() 28 | } 29 | } 30 | 31 | func RegistryDomainOrDefault(o types.Organization) string { 32 | if o.RegistryDomain != nil { 33 | return *o.RegistryDomain 34 | } else { 35 | return env.RegistryHost() 36 | } 37 | } 38 | 39 | func EmailFromAddressParsedOrDefault(o types.Organization) (*mail.Address, error) { 40 | if o.EmailFromAddress != nil { 41 | return mail.ParseAddress(*o.EmailFromAddress) 42 | } else { 43 | return util.PtrTo(env.GetMailerConfig().FromAddress), nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/db/dashboard.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/glasskube/distr/internal/apierrors" 8 | internalctx "github.com/glasskube/distr/internal/context" 9 | "github.com/google/uuid" 10 | "github.com/jackc/pgx/v5" 11 | ) 12 | 13 | func GetLatestPullOfArtifactByUser(ctx context.Context, artifactId uuid.UUID, userId uuid.UUID) (string, error) { 14 | db := internalctx.GetDb(ctx) 15 | if rows, err := db.Query(ctx, ` 16 | SELECT av.name 17 | FROM ArtifactVersionPull avpl 18 | JOIN ArtifactVersion av ON av.id = avpl.artifact_version_id 19 | WHERE av.artifact_id = @artifactId 20 | AND avpl.useraccount_id = @userId 21 | AND av.name NOT LIKE '%:%' 22 | ORDER BY avpl.created_at DESC 23 | LIMIT 1; 24 | `, pgx.NamedArgs{ 25 | "artifactId": artifactId, 26 | "userId": userId, 27 | }); err != nil { 28 | return "", err 29 | } else if res, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByPos[struct{ Name string }]); err != nil { 30 | if errors.Is(err, pgx.ErrNoRows) { 31 | return "", apierrors.ErrNotFound 32 | } 33 | return "", err 34 | } else { 35 | return res.Name, nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/db/queryable/interface.go: -------------------------------------------------------------------------------- 1 | package queryable 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgconn" 8 | ) 9 | 10 | type Queryable interface { 11 | Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) 12 | Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) 13 | QueryRow(ctx context.Context, sql string, args ...any) pgx.Row 14 | CopyFrom( 15 | ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource, 16 | ) (int64, error) 17 | Begin(ctx context.Context) (pgx.Tx, error) 18 | } 19 | -------------------------------------------------------------------------------- /internal/db/tx.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | internalctx "github.com/glasskube/distr/internal/context" 8 | "github.com/jackc/pgx/v5" 9 | "go.uber.org/multierr" 10 | ) 11 | 12 | func RunTx(ctx context.Context, f func(ctx context.Context) error) (finalErr error) { 13 | db := internalctx.GetDb(ctx) 14 | if tx, err := db.Begin(ctx); err != nil { 15 | return err 16 | } else { 17 | defer func() { 18 | // Rollback is safe to call after commit but we have to silence ErrTxClosed 19 | if err := tx.Rollback(ctx); !errors.Is(err, pgx.ErrTxClosed) { 20 | multierr.AppendInto(&finalErr, err) 21 | } 22 | }() 23 | if err := f(internalctx.WithDb(ctx, tx)); err != nil { 24 | return err 25 | } else { 26 | return tx.Commit(ctx) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/envparse/envparse.go: -------------------------------------------------------------------------------- 1 | package envparse 2 | 3 | import ( 4 | "errors" 5 | "net/mail" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func PositiveDuration(value string) (time.Duration, error) { 11 | parsed, err := time.ParseDuration(value) 12 | if err == nil && parsed.Nanoseconds() <= 0 { 13 | err = errors.New("duration must be positive") 14 | } 15 | return parsed, err 16 | } 17 | 18 | func ByteSlice(s string) ([]byte, error) { 19 | return []byte(s), nil 20 | } 21 | 22 | func MailAddress(s string) (mail.Address, error) { 23 | if parsed, err := mail.ParseAddress(s); err != nil || parsed == nil { 24 | return mail.Address{}, err 25 | } else { 26 | return *parsed, nil 27 | } 28 | } 29 | 30 | func NonNegativeNumber(value string) (int, error) { 31 | parsed, err := strconv.Atoi(value) 32 | if err == nil && parsed < 0 { 33 | err = errors.New("number must not be negative") 34 | } 35 | return parsed, err 36 | } 37 | 38 | func Float(value string) (float64, error) { 39 | return strconv.ParseFloat(value, 64) 40 | } 41 | -------------------------------------------------------------------------------- /internal/frontend/dist/.gitignore: -------------------------------------------------------------------------------- 1 | */ 2 | -------------------------------------------------------------------------------- /internal/frontend/fs.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | ) 7 | 8 | //go:embed dist/* 9 | var embeddedFsys embed.FS 10 | 11 | func BrowserFS() fs.FS { 12 | if fs, err := fs.Sub(embeddedFsys, "dist/ui/browser"); err != nil { 13 | panic(err) 14 | } else { 15 | return fs 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/handlers/agent_versions.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/getsentry/sentry-go" 7 | internalctx "github.com/glasskube/distr/internal/context" 8 | "github.com/glasskube/distr/internal/db" 9 | "github.com/go-chi/chi/v5" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func AgentVersionsRouter(r chi.Router) { 14 | r.Get("/", getAgentVersionsHandler()) 15 | } 16 | 17 | func getAgentVersionsHandler() http.HandlerFunc { 18 | return func(w http.ResponseWriter, r *http.Request) { 19 | ctx := r.Context() 20 | log := internalctx.GetLogger(ctx) 21 | if agentVersions, err := db.GetAgentVersions(ctx); err != nil { 22 | log.Warn("could not get agent versions", zap.Error(err)) 23 | sentry.GetHubFromContext(ctx).CaptureException(err) 24 | http.Error(w, err.Error(), http.StatusInternalServerError) 25 | } else { 26 | RespondJSON(w, agentVersions) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/handlers/deployment_target_metrics.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/getsentry/sentry-go" 7 | "github.com/glasskube/distr/internal/auth" 8 | internalctx "github.com/glasskube/distr/internal/context" 9 | "github.com/glasskube/distr/internal/db" 10 | "github.com/glasskube/distr/internal/middleware" 11 | "github.com/go-chi/chi/v5" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func DeploymentTargetMetricsRouter(r chi.Router) { 16 | r.Use(middleware.RequireOrgAndRole, requireUserRoleVendor) 17 | r.Get("/", getLatestDeploymentTargetMetrics) 18 | } 19 | 20 | func getLatestDeploymentTargetMetrics(w http.ResponseWriter, r *http.Request) { 21 | ctx := r.Context() 22 | auth := auth.Authentication.Require(ctx) 23 | if deploymentTargetMetrics, err := db.GetLatestDeploymentTargetMetrics(ctx, 24 | *auth.CurrentOrgID(), 25 | auth.CurrentUserID(), 26 | *auth.CurrentUserRole()); err != nil { 27 | internalctx.GetLogger(ctx).Error("failed to get deployment target metrics", zap.Error(err)) 28 | sentry.GetHubFromContext(ctx).CaptureException(err) 29 | w.WriteHeader(http.StatusInternalServerError) 30 | } else { 31 | RespondJSON(w, deploymentTargetMetrics) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/handlers/static.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "io/fs" 5 | "net/http" 6 | ) 7 | 8 | func StaticFileHandler(fsys fs.FS) http.HandlerFunc { 9 | server := http.FileServer(http.FS(fsys)) 10 | return func(w http.ResponseWriter, r *http.Request) { 11 | // check if the requested file exists and use index.html if it does not. 12 | if _, err := fs.Stat(fsys, r.URL.Path[1:]); err != nil { 13 | http.StripPrefix(r.URL.Path, server).ServeHTTP(w, r) 14 | } else { 15 | server.ServeHTTP(w, r) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/jobs/job.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import "context" 4 | 5 | type JobFunc func(context.Context) error 6 | 7 | type Job struct { 8 | name string 9 | runFunc JobFunc 10 | } 11 | 12 | func NewJob(name string, runFunc JobFunc) Job { 13 | return Job{name: name, runFunc: runFunc} 14 | } 15 | 16 | func (job *Job) Run(ctx context.Context) error { 17 | return job.runFunc(ctx) 18 | } 19 | -------------------------------------------------------------------------------- /internal/jobs/logger.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | type gocronLoggerAdapter struct { 8 | logger *zap.SugaredLogger 9 | } 10 | 11 | // Debug implements gocron.Logger. 12 | func (g *gocronLoggerAdapter) Debug(msg string, args ...any) { 13 | g.logger.With(args...).Debugf(msg) 14 | } 15 | 16 | // Error implements gocron.Logger. 17 | func (g *gocronLoggerAdapter) Error(msg string, args ...any) { 18 | g.logger.With(args...).Errorf(msg) 19 | } 20 | 21 | // Info implements gocron.Logger. 22 | func (g *gocronLoggerAdapter) Info(msg string, args ...any) { 23 | g.logger.With(args...).Infof(msg) 24 | } 25 | 26 | // Warn implements gocron.Logger. 27 | func (g *gocronLoggerAdapter) Warn(msg string, args ...any) { 28 | g.logger.With(args...).Warnf(msg) 29 | } 30 | -------------------------------------------------------------------------------- /internal/jobs/scheduler.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "github.com/glasskube/distr/internal/db/queryable" 5 | "github.com/go-co-op/gocron/v2" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | type Scheduler struct { 10 | scheduler gocron.Scheduler 11 | logger *zap.Logger 12 | runner *runner 13 | } 14 | 15 | func NewScheduler(logger *zap.Logger, db queryable.Queryable) (*Scheduler, error) { 16 | if scheduler, err := gocron.NewScheduler( 17 | gocron.WithLogger(&gocronLoggerAdapter{logger: logger.Sugar()}), 18 | ); err != nil { 19 | return nil, err 20 | } else { 21 | return &Scheduler{ 22 | scheduler: scheduler, 23 | logger: logger, 24 | runner: NewRunner(logger, db), 25 | }, nil 26 | } 27 | } 28 | 29 | func (s *Scheduler) RegisterCronJob(cron string, job Job) error { 30 | _, err := s.scheduler.NewJob( 31 | gocron.CronJob(cron, false), 32 | gocron.NewTask(s.runner.RunJobFunc(job)), 33 | gocron.WithName(job.name), 34 | gocron.WithSingletonMode(gocron.LimitModeReschedule), 35 | ) 36 | return err 37 | } 38 | 39 | func (s *Scheduler) Start() { 40 | s.logger.Info("job scheduler starting", zap.Int("jobs", len(s.scheduler.Jobs()))) 41 | s.scheduler.Start() 42 | } 43 | 44 | func (s *Scheduler) Shutdown() error { 45 | s.logger.Info("job scheduler shutting down") 46 | return s.scheduler.Shutdown() 47 | } 48 | -------------------------------------------------------------------------------- /internal/mail/mailer.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Mailer interface { 8 | Send(ctx context.Context, mail Mail) error 9 | } 10 | 11 | type FromAddressSrcFn = func(ctx context.Context, mail Mail) string 12 | 13 | type MailerConfig struct { 14 | FromAddressSrc []FromAddressSrcFn 15 | } 16 | 17 | func StaticFromAddress(address string) FromAddressSrcFn { 18 | return func(ctx context.Context, mail Mail) string { 19 | return address 20 | } 21 | } 22 | 23 | func MailOverrideFromAddress() FromAddressSrcFn { 24 | return func(ctx context.Context, mail Mail) string { 25 | if mail.From != nil { 26 | return mail.From.String() 27 | } 28 | return "" 29 | } 30 | } 31 | 32 | func (mc *MailerConfig) GetActualFromAddress(ctx context.Context, mail Mail) string { 33 | for _, fn := range mc.FromAddressSrc { 34 | if a := fn(ctx, mail); a != "" { 35 | return a 36 | } 37 | } 38 | return "" 39 | } 40 | -------------------------------------------------------------------------------- /internal/mail/noop/mailer.go: -------------------------------------------------------------------------------- 1 | package noop 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/glasskube/distr/internal/mail" 7 | ) 8 | 9 | type mailer struct{} 10 | 11 | // Send implements mail.Mailer by doing nothing at all 12 | func (m *mailer) Send(ctx context.Context, mail mail.Mail) error { 13 | return nil 14 | } 15 | 16 | var _ mail.Mailer = &mailer{} 17 | 18 | func New() *mailer { return &mailer{} } 19 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/fragments/footer.html: -------------------------------------------------------------------------------- 1 |
Distr
2 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/fragments/greeting.html: -------------------------------------------------------------------------------- 1 | Dear customer, 2 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/fragments/header.html: -------------------------------------------------------------------------------- 1 |
{{ template "fragments/logo.html" . }}
2 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/fragments/signature.html: -------------------------------------------------------------------------------- 1 | Best regards,
2 | the Distr Team! 3 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/invite-customer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "fragments/style.html" }} 6 | 7 | 8 |
9 | {{ template "fragments/header.html" . }} 10 |
11 |

Hi,

12 | 13 |

{{.Organization.Name}} invited you to install and manage applications via Distr.

14 | 15 |

16 | {{ if .InviteURL }} To finish your setup and get further instructions please click 17 | here. {{ else }} Log in with your existing account at 18 | {{ .Host }} and explore their Customer Portal in the top right corner. {{ end }} 19 |

20 | 21 |

{{template "fragments/signature.html"}}

22 |
23 | {{template "fragments/footer.html"}} 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/invite-user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "fragments/style.html" }} 6 | 7 | 8 |
9 | {{ template "fragments/header.html" . }} 10 |
11 |

Hi,

12 | 13 |

14 | Your e-mail address has been added to the {{.Organization.Name}} organization on 15 | Distr. 16 |

17 | 18 | {{ if .InviteURL }} 19 |

20 | Since you don't have an account yet, please finish your setup by clicking 21 | here. 22 |

23 | {{ else }} 24 |

25 | Log in with your existing account at {{ .Host }} and explore their Vendor Portal in 26 | the top right corner. 27 |

28 | {{ end }} 29 | 30 |

{{template "fragments/signature.html"}}

31 |
32 | {{template "fragments/footer.html"}} 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/password-reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "fragments/style.html" }} 6 | 7 | 8 |
9 | {{ template "fragments/header.html" . }} 10 |
11 | {{if .UserAccount.Name}} 12 |

Hi {{.UserAccount.Name}}

13 | {{else}} 14 |

Hi,

15 | {{end}} 16 | 17 |

18 | A password reset was requested for this email address on Distr. If this was you, please 19 | click here to complete the reset. 20 |

21 | 22 |
23 | {{.Host}}/reset?jwt={{.Token | QueryEscape}} 24 |
25 | 26 |

{{template "fragments/signature.html"}}

27 |
28 | {{template "fragments/footer.html"}} 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /internal/mailtemplates/templates/verify-email-registration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "fragments/style.html" }} 6 | 7 | 8 |
9 | {{ template "fragments/header.html" . }} 10 |
11 | {{if .UserAccount.Name}} 12 |

Hi {{.UserAccount.Name}}

13 | {{else}} 14 |

Hi,

15 | {{end}} Welcome to Distr! Please 16 | click this link to verify that this is your email 17 | address. 18 | 19 |

Alternatively, you can copy and paste the following URL into your browser:

20 | 21 |
22 | {{.Host}}/verify?jwt={{.Token | QueryEscape}} 23 |
24 | 25 |

{{template "fragments/signature.html"}}

26 |
27 | {{template "fragments/footer.html"}} 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /internal/mapping/access_token.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | import ( 4 | "github.com/glasskube/distr/api" 5 | "github.com/glasskube/distr/internal/types" 6 | ) 7 | 8 | func AccessTokenToDTO(model types.AccessToken) api.AccessToken { 9 | return api.AccessToken{ 10 | ID: model.ID, 11 | CreatedAt: model.CreatedAt, 12 | ExpiresAt: model.ExpiresAt, 13 | LastUsedAt: model.LastUsedAt, 14 | Label: model.Label, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/mapping/list.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | func List[IN any, OUT any](in []IN, mapping func(in IN) OUT) []OUT { 4 | out := make([]OUT, len(in)) 5 | for i, el := range in { 6 | out[i] = mapping(el) 7 | } 8 | return out 9 | } 10 | -------------------------------------------------------------------------------- /internal/migrations/sql/0_initial.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS DeploymentStatus CASCADE; 2 | DROP TABLE IF EXISTS Deployment CASCADE; 3 | DROP TABLE IF EXISTS DeploymentTargetStatus CASCADE; 4 | DROP TABLE IF EXISTS DeploymentTarget CASCADE; 5 | DROP TABLE IF EXISTS ApplicationVersion CASCADE; 6 | DROP TABLE IF EXISTS Application CASCADE; 7 | DROP TABLE IF EXISTS Organization_UserAccount CASCADE; 8 | DROP TABLE IF EXISTS UserAccount CASCADE; 9 | DROP TABLE IF EXISTS Organization CASCADE; 10 | DROP TYPE IF EXISTS DEPLOYMENT_TYPE CASCADE; 11 | DROP TYPE IF EXISTS USER_ROLE CASCADE; 12 | -------------------------------------------------------------------------------- /internal/migrations/sql/10_deployment_revision_environment.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentRevision DROP COLUMN env_file_data; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/10_deployment_revision_environment.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentRevision ADD COLUMN env_file_data BYTEA; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/11_deployment_user_no_cascade.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget 2 | DROP CONSTRAINT deploymenttarget_created_by_user_account_id_fkey, 3 | ADD CONSTRAINT deploymenttarget_created_by_user_account_id_fkey 4 | FOREIGN KEY (created_by_user_account_id) REFERENCES UserAccount (id) ON DELETE CASCADE; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/11_deployment_user_no_cascade.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget 2 | DROP CONSTRAINT deploymenttarget_created_by_user_account_id_fkey, 3 | ADD CONSTRAINT deploymenttarget_created_by_user_account_id_fkey 4 | FOREIGN KEY (created_by_user_account_id) REFERENCES UserAccount (id) ON DELETE RESTRICT; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/12_application_license.down.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE Deployment DROP COLUMN application_license_id; 3 | DROP TABLE ApplicationLicense_ApplicationVersion; 4 | DROP TABLE ApplicationLicense; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/13_org_feature_flags.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organization DROP COLUMN features; 2 | 3 | DROP TYPE FEATURE; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/13_org_feature_flags.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE FEATURE AS ENUM ('licensing'); 2 | 3 | ALTER TABLE organization ADD COLUMN features FEATURE[] DEFAULT ARRAY[]::FEATURE[] NOT NULL; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/14_deployment_revision_created_at_index.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX DeploymentRevisionStatus_created_at; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/14_deployment_revision_created_at_index.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX DeploymentRevisionStatus_created_at ON DeploymentRevisionStatus (deployment_revision_id, created_at DESC); 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/15_deployment_target_created_at_index.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX DeploymentTargetStatus_created_at; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/15_deployment_target_created_at_index.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX DeploymentTargetStatus_created_at ON DeploymentTargetStatus (deployment_target_id, created_at DESC); 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/16_deployment_application_no_cascade.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentRevision 2 | DROP CONSTRAINT deploymentrevision_application_version_id_fkey, 3 | ADD CONSTRAINT deploymentrevision_application_version_id_fkey 4 | FOREIGN KEY (application_version_id) REFERENCES applicationversion(id) ON DELETE CASCADE; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/16_deployment_application_no_cascade.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentRevision 2 | DROP CONSTRAINT deploymentrevision_application_version_id_fkey, 3 | ADD CONSTRAINT deploymentrevision_application_version_id_fkey 4 | FOREIGN KEY (application_version_id) REFERENCES applicationversion(id) ON DELETE RESTRICT; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/17_application_version_name_unique.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ApplicationVersion 2 | DROP CONSTRAINT name_unique; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/17_application_version_name_unique.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ApplicationVersion 2 | ADD CONSTRAINT name_unique UNIQUE (application_id, name); 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/18_application_version_archive.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ApplicationVersion DROP COLUMN archived_at; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/18_application_version_archive.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ApplicationVersion ADD COLUMN archived_at TIMESTAMP; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/19_organization_slug.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX Organization_slug; 2 | 3 | ALTER TABLE Organization DROP COLUMN slug; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/19_organization_slug.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organization ADD COLUMN slug TEXT UNIQUE; 2 | 3 | CREATE INDEX IF NOT EXISTS Organization_slug ON Organization (slug); 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/1_helm_deployments.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ApplicationVersion 2 | DROP COLUMN chart_type, 3 | DROP COLUMN chart_name, 4 | DROP COLUMN chart_url, 5 | DROP COLUMN chart_version, 6 | DROP COLUMN values_file_data, 7 | DROP COLUMN template_file_data; 8 | 9 | DROP TYPE IF EXISTS HELM_CHART_TYPE CASCADE; 10 | -------------------------------------------------------------------------------- /internal/migrations/sql/1_helm_deployments.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE HELM_CHART_TYPE AS ENUM ('repository', 'oci'); 2 | 3 | ALTER TABLE ApplicationVersion 4 | ADD COLUMN chart_type HELM_CHART_TYPE, 5 | ADD COLUMN chart_name TEXT, 6 | ADD COLUMN chart_url TEXT, 7 | ADD COLUMN chart_version TEXT, 8 | ADD COLUMN values_file_data BYTEA, 9 | ADD COLUMN template_file_data BYTEA; 10 | -------------------------------------------------------------------------------- /internal/migrations/sql/20_artifacts.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE ArtifactLicense_Artifact; 2 | DROP TABLE ArtifactLicense; 3 | 4 | DROP TABLE ArtifactVersionPart; 5 | DROP TABLE ArtifactVersion; 6 | DROP TABLE Artifact; 7 | -------------------------------------------------------------------------------- /internal/migrations/sql/21_artifact_license_expiry.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactLicense DROP COLUMN expires_at; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/21_artifact_license_expiry.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactLicense ADD COLUMN expires_at TIMESTAMP; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/22_artifact_license_org.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactLicense DROP COLUMN organization_id; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/22_artifact_license_org.up.sql: -------------------------------------------------------------------------------- 1 | -- a bit dirty because it only works if there are no licenses yet, but since this feature isn't released yet, should be fine 2 | ALTER TABLE ArtifactLicense 3 | ADD COLUMN organization_id UUID NOT NULL REFERENCES Organization (id), 4 | ADD CONSTRAINT ArtifactLicense_name_unique UNIQUE (organization_id, name); 5 | 6 | CREATE INDEX fk_ArtifactLicense_organization_id ON ArtifactLicense (organization_id); 7 | -------------------------------------------------------------------------------- /internal/migrations/sql/23_artifacts_audit_log.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE ArtifactVersionPull; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/23_artifacts_audit_log.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE ArtifactVersionPull ( 2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 | created_at TIMESTAMP DEFAULT current_timestamp, 4 | artifact_version_id UUID NOT NULL REFERENCES ArtifactVersion (id) ON DELETE CASCADE, 5 | useraccount_id UUID REFERENCES UserAccount (id) ON DELETE SET NULL 6 | ); 7 | 8 | CREATE INDEX fk_ArtifactVersionPull_artifact_version_id ON ArtifactVersionPull (artifact_version_id); 9 | -------------------------------------------------------------------------------- /internal/migrations/sql/24_registry_feature_flag.down.sql: -------------------------------------------------------------------------------- 1 | -- ALTER TYPE FEATURE DROP VALUE is not supported by postgres 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/24_registry_feature_flag.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE FEATURE ADD VALUE IF NOT EXISTS 'registry'; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/25_artifact_size.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactVersion DROP COLUMN manifest_blob_size; 2 | ALTER TABLE ArtifactVersionPart DROP COLUMN artifact_blob_size; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/25_artifact_size.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactVersion ADD COLUMN manifest_blob_size BIGINT NOT NULL DEFAULT 0; 2 | ALTER TABLE ArtifactVersionPart ADD COLUMN artifact_blob_size BIGINT NOT NULL DEFAULT 0; 3 | ALTER TABLE ArtifactVersion ALTER COLUMN manifest_blob_size DROP DEFAULT; 4 | ALTER TABLE ArtifactVersionPart ALTER COLUMN artifact_blob_size DROP DEFAULT; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/26_organization_artifact_version_quota.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organization DROP COLUMN artifact_tag_limit; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/26_organization_artifact_version_quota.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organization ADD COLUMN artifact_tag_limit INT DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/27_deployment_revision_status_index.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX DeploymentRevisionStatus_created_at_single; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/27_deployment_revision_status_index.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX DeploymentRevisionStatus_created_at_single ON DeploymentRevisionStatus (created_at DESC); 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/28_artifact_blob_digest_indices.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX ArtifactVersionPart_artifact_blob_digest; 2 | DROP INDEX ArtifactVersion_manifest_blob_digest; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/28_artifact_blob_digest_indices.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX ArtifactVersionPart_artifact_blob_digest ON ArtifactVersionPart (artifact_blob_digest); 2 | CREATE INDEX ArtifactVersion_manifest_blob_digest ON ArtifactVersion (manifest_blob_digest); 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/29_user_logged_in_at.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE UserAccount 2 | DROP COLUMN last_logged_in_at; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/29_user_logged_in_at.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE UserAccount 2 | ADD COLUMN last_logged_in_at TIMESTAMP DEFAULT NULL; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/2_kubernetes_deployment.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget 2 | DROP COLUMN namespace; 3 | 4 | ALTER TABLE Deployment 5 | DROP CONSTRAINT release_name_unique, 6 | DROP COLUMN release_name, 7 | DROP COLUMN values_yaml; 8 | -------------------------------------------------------------------------------- /internal/migrations/sql/2_kubernetes_deployment.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget 2 | ADD COLUMN namespace TEXT; 3 | 4 | ALTER TABLE Deployment 5 | ADD COLUMN release_name TEXT, 6 | ADD COLUMN values_yaml BYTEA, 7 | ADD CONSTRAINT release_name_unique UNIQUE (deployment_target_id, release_name); 8 | -------------------------------------------------------------------------------- /internal/migrations/sql/30_artifacts_audit_log_ip.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactVersionPull 2 | DROP COLUMN remote_address; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/30_artifacts_audit_log_ip.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ArtifactVersionPull 2 | ADD COLUMN remote_address TEXT DEFAULT NULL; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/31_status_type_progressing.down.sql: -------------------------------------------------------------------------------- 1 | -- Credits: https://stackoverflow.com/a/25812436 2 | 3 | ALTER TYPE DEPLOYMENT_STATUS_TYPE 4 | RENAME TO DEPLOYMENT_STATUS_TYPE_OLD; 5 | 6 | CREATE TYPE DEPLOYMENT_STATUS_TYPE AS ENUM ('ok', 'error'); 7 | 8 | ALTER TABLE DeploymentRevisionStatus 9 | ALTER COLUMN type TYPE DEPLOYMENT_STATUS_TYPE 10 | USING (type::text::DEPLOYMENT_STATUS_TYPE); 11 | 12 | DROP TYPE DEPLOYMENT_STATUS_TYPE_OLD; 13 | -------------------------------------------------------------------------------- /internal/migrations/sql/31_status_type_progressing.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE DEPLOYMENT_STATUS_TYPE 2 | ADD VALUE 'progressing'; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/32_images.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE UserAccount 2 | DROP COLUMN image_id; 3 | 4 | ALTER TABLE Application 5 | DROP COLUMN image_id; 6 | 7 | ALTER TABLE Artifact 8 | DROP COLUMN image_id; 9 | 10 | DROP TABLE IF EXISTS File; 11 | -------------------------------------------------------------------------------- /internal/migrations/sql/32_images.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS File 2 | ( 3 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 4 | created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, 5 | organization_id UUID NOT NULL REFERENCES Organization (id) ON DELETE CASCADE, 6 | content_type TEXT NOT NULL, 7 | data BYTEA NOT NULL, 8 | file_name TEXT NOT NULL, 9 | file_size INT NOT NULL 10 | ); 11 | 12 | CREATE INDEX IF NOT EXISTS fk_File_organization_id ON File (organization_id); 13 | 14 | 15 | ALTER TABLE UserAccount 16 | ADD COLUMN image_id UUID DEFAULT NULL REFERENCES File (id) ON DELETE SET NULL; 17 | 18 | ALTER TABLE Application 19 | ADD COLUMN image_id UUID DEFAULT NULL REFERENCES File (id) ON DELETE SET NULL; 20 | 21 | ALTER TABLE Artifact 22 | ADD COLUMN image_id UUID DEFAULT NULL REFERENCES File (id) ON DELETE SET NULL; 23 | -------------------------------------------------------------------------------- /internal/migrations/sql/33_tutorials.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS UserAccount_TutorialProgress; 2 | 3 | DROP TYPE IF EXISTS TUTORIAL; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/33_tutorials.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE TUTORIAL AS ENUM ('branding', 'agents', 'registry'); 2 | 3 | CREATE TABLE UserAccount_TutorialProgress ( 4 | useraccount_id UUID NOT NULL REFERENCES UserAccount(id) ON DELETE CASCADE, 5 | tutorial TUTORIAL NOT NULL, 6 | events JSONB, 7 | created_at TIMESTAMP DEFAULT current_timestamp, 8 | completed_at TIMESTAMP, 9 | PRIMARY KEY (useraccount_id, tutorial) 10 | ); 11 | 12 | CREATE INDEX IF NOT EXISTS UserAccount_TutorialProgress_useraccount_id ON UserAccount_TutorialProgress(useraccount_id); 13 | -------------------------------------------------------------------------------- /internal/migrations/sql/34_remove_registry_feature.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE FEATURE 2 | ADD VALUE 'registry'; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/34_remove_registry_feature.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE FEATURE RENAME TO FEATURE_OLD; 2 | 3 | CREATE TYPE FEATURE AS ENUM ('licensing'); 4 | 5 | UPDATE Organization SET features = array_remove(features, 'registry') WHERE 'registry' = ANY(features); 6 | 7 | ALTER TABLE Organization ALTER COLUMN features DROP DEFAULT; -- otherwise the following wouldnt work: 8 | ALTER TABLE Organization 9 | ALTER COLUMN features TYPE FEATURE[] 10 | USING (features::text[]::FEATURE[]); 11 | ALTER TABLE Organization 12 | ALTER COLUMN features SET DEFAULT ARRAY[]::FEATURE[]; 13 | 14 | DROP TYPE FEATURE_OLD; 15 | -------------------------------------------------------------------------------- /internal/migrations/sql/35_docker_type.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Deployment 2 | DROP COLUMN docker_type; 3 | 4 | DROP TYPE DOCKER_TYPE; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/35_docker_type.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE DOCKER_TYPE AS ENUM ('compose', 'swarm'); 2 | 3 | ALTER TABLE Deployment 4 | ADD COLUMN docker_type DOCKER_TYPE; 5 | 6 | UPDATE Deployment d 7 | SET docker_type = 'compose' 8 | FROM DeploymentTarget dt 9 | WHERE d.deployment_target_id = dt.id 10 | AND dt.type = 'docker'; 11 | -------------------------------------------------------------------------------- /internal/migrations/sql/36_organization_custom_domain.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Organization 2 | DROP COLUMN app_domain, 3 | DROP COLUMN registry_domain, 4 | DROP COLUMN email_from_address; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/36_organization_custom_domain.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Organization 2 | ADD COLUMN app_domain TEXT DEFAULT NULL, 3 | ADD COLUMN registry_domain TEXT DEFAULT NULL, 4 | ADD COLUMN email_from_address TEXT DEFAULT NULL; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/37_deploymenttarget_metrics.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget DROP COLUMN metrics_enabled; 2 | 3 | DROP TABLE IF EXISTS DeploymentTargetMetrics; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/37_deploymenttarget_metrics.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget ADD COLUMN metrics_enabled BOOLEAN NOT NULL DEFAULT true; 2 | 3 | UPDATE DeploymentTarget SET metrics_enabled = false WHERE scope = 'namespace'; 4 | 5 | CREATE TABLE DeploymentTargetMetrics ( 6 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 7 | created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, 8 | deployment_target_id UUID NOT NULL REFERENCES DeploymentTarget(id) ON DELETE CASCADE, 9 | cpu_cores_millis BIGINT NOT NULL, 10 | cpu_usage FLOAT NOT NULL, 11 | memory_bytes BIGINT NOT NULL, 12 | memory_usage FLOAT NOT NULL 13 | ); 14 | 15 | CREATE INDEX IF NOT EXISTS DeploymentTargetMetrics_created_at ON DeploymentRevisionStatus (created_at DESC); 16 | 17 | CREATE INDEX IF NOT EXISTS DeploymentTargetMetrics_deployment_target_id ON DeploymentTargetMetrics(deployment_target_id); 18 | -------------------------------------------------------------------------------- /internal/migrations/sql/38_fix_tutorial_docker_type.down.sql: -------------------------------------------------------------------------------- 1 | -- not possible to revert 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/38_fix_tutorial_docker_type.up.sql: -------------------------------------------------------------------------------- 1 | -- set docker type compose for accidentally created docker deployments without docker type (hello-distr-tutorial bug) 2 | 3 | UPDATE Deployment d 4 | SET docker_type = 'compose' 5 | FROM DeploymentTarget dt 6 | WHERE d.deployment_target_id = dt.id 7 | AND dt.type = 'docker' AND d.docker_type IS NULL; 8 | -------------------------------------------------------------------------------- /internal/migrations/sql/39_deployment_logs.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE DeploymentLogRecord; 2 | 3 | ALTER TABLE Deployment 4 | DROP COLUMN logs_enabled; 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/39_deployment_logs.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE DeploymentLogRecord ( 2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 | created_at TIMESTAMP DEFAULT current_timestamp, 4 | deployment_id UUID NOT NULL REFERENCES Deployment(id) ON DELETE CASCADE, 5 | deployment_revision_id UUID NOT NULL REFERENCES DeploymentRevision(id) ON DELETE CASCADE, 6 | resource TEXT, 7 | timestamp TIMESTAMP, 8 | severity TEXT, 9 | body TEXT 10 | ); 11 | 12 | CREATE INDEX fk_DeploymentLogRecord_deployment_id ON DeploymentLogRecord (deployment_id); 13 | CREATE INDEX fk_DeploymentLogRecord_deployment_revision_id ON DeploymentLogRecord (deployment_revision_id); 14 | CREATE INDEX DeploymentLogRecord_created_at ON DeploymentLogRecord (deployment_id, created_at); 15 | 16 | ALTER TABLE Deployment 17 | ADD COLUMN logs_enabled BOOLEAN NOT NULL DEFAULT false; 18 | -------------------------------------------------------------------------------- /internal/migrations/sql/3_remove_release_name_constraint.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Deployment 2 | ADD CONSTRAINT release_name_unique UNIQUE (deployment_target_id, release_name); 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/3_remove_release_name_constraint.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Deployment 2 | DROP CONSTRAINT release_name_unique; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/40_multi_org_support.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Organization_UserAccount DROP COLUMN created_at; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/40_multi_org_support.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Organization_UserAccount ADD COLUMN created_at TIMESTAMP DEFAULT current_timestamp; 2 | 3 | UPDATE Organization_UserAccount oua 4 | SET created_at = ua.created_at 5 | FROM UserAccount ua 6 | WHERE ua.id = oua.user_account_id; 7 | -------------------------------------------------------------------------------- /internal/migrations/sql/41_pat_organization_scoped.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE AccessToken DROP COLUMN organization_id; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/41_pat_organization_scoped.up.sql: -------------------------------------------------------------------------------- 1 | -- this migration assumes that every user is part of exactly one organization 2 | -- if there is a user without an organization, this will fail. a manual fix would be to delete the corresponding AccessToken 3 | 4 | ALTER TABLE AccessToken ADD COLUMN organization_id UUID REFERENCES Organization(id) ON DELETE CASCADE; 5 | 6 | UPDATE AccessToken at 7 | SET organization_id = oua.organization_id 8 | FROM Organization_UserAccount oua 9 | WHERE oua.user_account_id = at.user_account_id; 10 | 11 | ALTER TABLE AccessToken ALTER COLUMN organization_id SET NOT NULL; 12 | 13 | CREATE INDEX IF NOT EXISTS fk_AccessToken_organization_id ON AccessToken (organization_id); 14 | -------------------------------------------------------------------------------- /internal/migrations/sql/42_tutorials_organization_scoped.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE UserAccount_TutorialProgress DROP CONSTRAINT useraccount_tutorialprogress_pkey; 2 | DROP INDEX IF EXISTS useraccount_tutorialprogress_pkey; 3 | ALTER TABLE UserAccount_TutorialProgress DROP COLUMN organization_id; 4 | ALTER TABLE UserAccount_TutorialProgress ADD CONSTRAINT useraccount_tutorialprogress_pkey PRIMARY KEY (useraccount_id, tutorial); 5 | -------------------------------------------------------------------------------- /internal/migrations/sql/42_tutorials_organization_scoped.up.sql: -------------------------------------------------------------------------------- 1 | -- this migration assumes that every user is part of exactly one organization 2 | -- if there is a user without an organization, this will fail. a manual fix would be to delete the corresponding UserAccount_TutorialProgress 3 | 4 | ALTER TABLE UserAccount_TutorialProgress ADD COLUMN organization_id UUID REFERENCES Organization(id) ON DELETE CASCADE; 5 | 6 | ALTER TABLE UserAccount_TutorialProgress DROP CONSTRAINT useraccount_tutorialprogress_pkey; 7 | 8 | UPDATE UserAccount_TutorialProgress ut 9 | SET organization_id = oua.organization_id 10 | FROM Organization_UserAccount oua 11 | WHERE oua.user_account_id = ut.useraccount_id; 12 | 13 | ALTER TABLE UserAccount_TutorialProgress ALTER COLUMN organization_id SET NOT NULL; 14 | ALTER TABLE UserAccount_TutorialProgress ADD CONSTRAINT useraccount_tutorialprogress_pkey PRIMARY KEY (useraccount_id, tutorial, organization_id); 15 | 16 | CREATE INDEX IF NOT EXISTS fk_UserAccount_TutorialProgress_organization_id ON UserAccount_TutorialProgress (organization_id); 17 | -------------------------------------------------------------------------------- /internal/migrations/sql/43_file_org_nullable.down.sql: -------------------------------------------------------------------------------- 1 | UPDATE File f 2 | SET organization_id = oua.organization_id 3 | FROM Organization_UserAccount oua 4 | INNER JOIN UserAccount u ON oua.user_account_id = u.id 5 | WHERE u.image_id = f.id; 6 | 7 | ALTER TABLE File ALTER COLUMN organization_id SET NOT NULL; 8 | -------------------------------------------------------------------------------- /internal/migrations/sql/43_file_org_nullable.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE File ALTER COLUMN organization_id DROP NOT NULL; 2 | 3 | UPDATE File f 4 | SET organization_id = NULL 5 | FROM UserAccount u 6 | WHERE u.image_id = f.id; 7 | -------------------------------------------------------------------------------- /internal/migrations/sql/44_remove_deployment_target_location.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget ADD COLUMN geolocation_lat FLOAT; 2 | ALTER TABLE DeploymentTarget ADD COLUMN geolocation_lon FLOAT; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/44_remove_deployment_target_location.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget DROP COLUMN geolocation_lat; 2 | ALTER TABLE DeploymentTarget DROP COLUMN geolocation_lon; 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/4_status_type.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentStatus DROP COLUMN type; 2 | 3 | DROP TYPE IF EXISTS DEPLOYMENT_STATUS_TYPE CASCADE; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/4_status_type.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE DEPLOYMENT_STATUS_TYPE AS ENUM ('ok', 'error'); 2 | 3 | ALTER TABLE DeploymentStatus ADD COLUMN type DEPLOYMENT_STATUS_TYPE NOT NULL DEFAULT 'ok'; 4 | -------------------------------------------------------------------------------- /internal/migrations/sql/5_organization_branding.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS OrganizationBranding CASCADE; 2 | 3 | -------------------------------------------------------------------------------- /internal/migrations/sql/5_organization_branding.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS OrganizationBranding 2 | ( 3 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 4 | created_at TIMESTAMP DEFAULT current_timestamp, 5 | organization_id UUID UNIQUE NOT NULL REFERENCES Organization (id) ON DELETE CASCADE, 6 | updated_at TIMESTAMP DEFAULT current_timestamp, 7 | updated_by_user_account_id UUID REFERENCES UserAccount (id) ON DELETE SET NULL, 8 | title TEXT, 9 | description TEXT, 10 | logo BYTEA, 11 | logo_file_name TEXT, 12 | logo_content_type TEXT 13 | ); 14 | 15 | CREATE INDEX IF NOT EXISTS fk_OrganizationBranding_organization_id ON OrganizationBranding (organization_id); 16 | -------------------------------------------------------------------------------- /internal/migrations/sql/6_deployment_revision.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE DeploymentRevision CASCADE; 2 | DROP TABLE DeploymentRevisionStatus; 3 | 4 | DELETE FROM Deployment; 5 | 6 | ALTER TABLE Deployment 7 | ADD COLUMN application_version_id UUID NOT NULL REFERENCES ApplicationVersion (id) ON DELETE CASCADE, 8 | ADD COLUMN values_yaml BYTEA; 9 | 10 | ALTER TABLE Deployment 11 | DROP CONSTRAINT release_name_unique; 12 | 13 | CREATE TABLE DeploymentStatus ( 14 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 15 | created_at TIMESTAMP DEFAULT current_timestamp, 16 | deployment_id UUID NOT NULL REFERENCES Deployment (id) ON DELETE CASCADE, 17 | message TEXT NOT NULL, 18 | type DEPLOYMENT_STATUS_TYPE NOT NULL DEFAULT 'ok' 19 | ); 20 | -------------------------------------------------------------------------------- /internal/migrations/sql/7_agent_version.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget 2 | DROP COLUMN agent_version_id, 3 | DROP COLUMN reported_agent_version_id; 4 | 5 | DROP TABLE AgentVersion; 6 | -------------------------------------------------------------------------------- /internal/migrations/sql/7_agent_version.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE AgentVersion ( 2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 | created_at TIMESTAMP DEFAULT current_timestamp, 4 | name TEXT NOT NULL UNIQUE, 5 | manifest_file_revision TEXT NOT NULL, 6 | compose_file_revision TEXT NOT NULL 7 | ); 8 | 9 | CREATE INDEX AgentVersion_name ON AgentVersion (name); 10 | 11 | ALTER TABLE DeploymentTarget 12 | ADD COLUMN agent_version_id UUID REFERENCES AgentVersion (id), 13 | ADD COLUMN reported_agent_version_id UUID REFERENCES AgentVersion (id) ON DELETE SET NULL; 14 | 15 | CREATE INDEX fk_DeploymentTarget_agent_version_id ON DeploymentTarget (agent_version_id); 16 | 17 | WITH inserted AS ( 18 | INSERT INTO AgentVersion (name, manifest_file_revision, compose_file_revision) 19 | VALUES ('0.8.2', 'v1', 'v1') 20 | RETURNING id 21 | ) 22 | UPDATE DeploymentTarget 23 | SET agent_version_id = inserted.id 24 | FROM inserted; 25 | 26 | ALTER TABLE DeploymentTarget 27 | ALTER COLUMN agent_version_id SET NOT NULL; 28 | -------------------------------------------------------------------------------- /internal/migrations/sql/8_deployment_target_scope.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE DeploymentTarget 2 | DROP CONSTRAINT scope_required, 3 | DROP CONSTRAINT namespace_required, 4 | DROP COLUMN scope; 5 | 6 | DROP TYPE DEPLOYMENT_TARGET_SCOPE; 7 | -------------------------------------------------------------------------------- /internal/migrations/sql/8_deployment_target_scope.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE DEPLOYMENT_TARGET_SCOPE AS ENUM ('cluster', 'namespace'); 2 | 3 | ALTER TABLE DeploymentTarget 4 | ADD COLUMN scope DEPLOYMENT_TARGET_SCOPE; 5 | 6 | UPDATE DeploymentTarget SET scope = 'namespace' WHERE type = 'kubernetes'; 7 | 8 | ALTER TABLE DeploymentTarget 9 | ADD CONSTRAINT scope_required CHECK ((type = 'docker') = (scope IS NULL)), 10 | ADD CONSTRAINT namespace_required CHECK ((type = 'docker') = (namespace IS NULL)); 11 | -------------------------------------------------------------------------------- /internal/migrations/sql/9_access_keys.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE AccessToken; 2 | -------------------------------------------------------------------------------- /internal/migrations/sql/9_access_keys.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE AccessToken ( 2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 | created_at TIMESTAMP DEFAULT current_timestamp, 4 | expires_at TIMESTAMP, 5 | last_used_at TIMESTAMP, 6 | label TEXT, 7 | key BYTEA UNIQUE NOT NULL, 8 | user_account_id UUID NOT NULL 9 | REFERENCES UserAccount (id) ON DELETE CASCADE 10 | ); 11 | 12 | CREATE INDEX IF NOT EXISTS AccessToken_key ON AccessToken (key); 13 | CREATE INDEX IF NOT EXISTS fk_AccessToken_user_account_id ON AccessToken (user_account_id); 14 | -------------------------------------------------------------------------------- /internal/registry/README.md: -------------------------------------------------------------------------------- 1 | This package is an adaptation of the registry package from [`google/go-containerregistry`](https://github.com/google/go-containerregistry/), licensed under the Apache-2.0 license. 2 | -------------------------------------------------------------------------------- /internal/registry/audit/audit.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/glasskube/distr/internal/auth" 7 | internalctx "github.com/glasskube/distr/internal/context" 8 | "github.com/glasskube/distr/internal/db" 9 | "github.com/glasskube/distr/internal/registry/name" 10 | ) 11 | 12 | type ArtifactAuditor interface { 13 | AuditPull(ctx context.Context, name, reference string) error 14 | } 15 | 16 | type auditor struct{} 17 | 18 | func NewAuditor() ArtifactAuditor { 19 | return &auditor{} 20 | } 21 | 22 | // AuditPull implements ArtifactAuditor. 23 | func (a *auditor) AuditPull(ctx context.Context, nameStr string, reference string) error { 24 | auth := auth.ArtifactsAuthentication.Require(ctx) 25 | if name, err := name.Parse(nameStr); err != nil { 26 | return err 27 | } else if digestVersion, err := db.GetArtifactVersion(ctx, name.OrgName, name.ArtifactName, reference); err != nil { 28 | return err 29 | } else { 30 | return db.CreateArtifactPullLogEntry( 31 | ctx, 32 | digestVersion.ID, 33 | auth.CurrentUserID(), 34 | internalctx.GetRequestIPAddress(ctx), 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/registry/authz/error.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import "errors" 4 | 5 | var ErrAccessDenied = errors.New("access denied") 6 | -------------------------------------------------------------------------------- /internal/registry/blob/error.go: -------------------------------------------------------------------------------- 1 | package blob 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // RedirectError represents a signal that the blob handler doesn't have the blob 9 | // contents, but that those contents are at another location which registry 10 | // clients should redirect to. 11 | type RedirectError struct { 12 | // Location is the location to find the contents. 13 | Location string 14 | 15 | // Code is the HTTP redirect status code to return to clients. 16 | Code int 17 | } 18 | 19 | func (e RedirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) } 20 | 21 | // errNotFound represents an error locating the blob. 22 | var ErrNotFound = errors.New("not found") 23 | 24 | var ErrBadUpload = errors.New("bad upload") 25 | 26 | func NewErrBadUpload(msg string) error { 27 | return fmt.Errorf("%w: %v", ErrBadUpload, msg) 28 | } 29 | -------------------------------------------------------------------------------- /internal/registry/error/error.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import "errors" 4 | 5 | var ErrInvalidArtifactName = errors.New("invalid artifact name") 6 | -------------------------------------------------------------------------------- /internal/registry/manifest/errors.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNameUnknown = errors.New("unknown name") 7 | ErrManifestUnknown = errors.New("unknown manifest") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/registry/manifest/interface.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "context" 5 | 6 | v1 "github.com/google/go-containerregistry/pkg/v1" 7 | ) 8 | 9 | type ManifestHandler interface { 10 | List(ctx context.Context, n int) ([]string, error) 11 | // ListTags 12 | // 13 | // Spec for implementation: 14 | // 15 | // name: Name of the target repository. 16 | // 17 | // n: Limit the number of entries in each response. If not present, all entries will be returned. 18 | // 19 | // last: Result set will include values lexically after last. 20 | ListTags(ctx context.Context, name string, n int, last string) ([]string, error) 21 | ListDigests(ctx context.Context, name string) ([]v1.Hash, error) 22 | Get(ctx context.Context, name string, reference string) (*Manifest, error) 23 | Put(ctx context.Context, name string, reference string, manifest Manifest, blobs []Blob) error 24 | Delete(ctx context.Context, name string, reference string) error 25 | } 26 | -------------------------------------------------------------------------------- /internal/registry/manifest/types.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import v1 "github.com/google/go-containerregistry/pkg/v1" 4 | 5 | type Manifest struct { 6 | Blob Blob 7 | ContentType string 8 | } 9 | 10 | type Blob struct { 11 | Digest v1.Hash 12 | Size int64 13 | } 14 | -------------------------------------------------------------------------------- /internal/registry/name/name.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strings" 7 | 8 | registryerror "github.com/glasskube/distr/internal/registry/error" 9 | ) 10 | 11 | type Name struct { 12 | OrgName string 13 | ArtifactName string 14 | } 15 | 16 | func Parse(input string) (*Name, error) { 17 | if parts := strings.SplitN(input, "/", 2); len(parts) != 2 { 18 | return nil, fmt.Errorf("%w: %v", registryerror.ErrInvalidArtifactName, input) 19 | } else { 20 | return &Name{parts[0], parts[1]}, nil 21 | } 22 | } 23 | 24 | func (obj Name) String() string { 25 | return path.Join(obj.OrgName, obj.ArtifactName) 26 | } 27 | -------------------------------------------------------------------------------- /internal/resources/embedded.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | "text/template" 8 | 9 | "github.com/glasskube/distr/internal/util" 10 | ) 11 | 12 | var ( 13 | //go:embed embedded 14 | embeddedFs embed.FS 15 | fsys = util.Require(fs.Sub(embeddedFs, "embedded")) 16 | templates = map[string]*template.Template{} 17 | ) 18 | 19 | func Get(name string) ([]byte, error) { 20 | return fs.ReadFile(fsys, name) 21 | } 22 | 23 | func GetTemplate(name string) (*template.Template, error) { 24 | if tmpl, ok := templates[name]; ok { 25 | return tmpl, nil 26 | } else if tmpl, err := template.ParseFS(fsys, name); err != nil { 27 | return nil, fmt.Errorf("failed to parse template %v: %w", name, err) 28 | } else { 29 | templates[name] = tmpl 30 | return tmpl, nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/resources/embedded/agent/docker/v1/docker-compose.yaml.tmpl: -------------------------------------------------------------------------------- 1 | name: distr 2 | services: 3 | agent: 4 | network_mode: host 5 | restart: unless-stopped 6 | image: 'ghcr.io/glasskube/distr/docker-agent:{{ .agentVersion }}' 7 | environment: 8 | DISTR_TARGET_ID: '{{ .targetId }}' 9 | DISTR_TARGET_SECRET: '{{ .targetSecret }}' 10 | DISTR_LOGIN_ENDPOINT: '{{ .loginEndpoint }}' 11 | DISTR_MANIFEST_ENDPOINT: '{{ .manifestEndpoint }}' 12 | DISTR_RESOURCE_ENDPOINT: '{{ .resourcesEndpoint }}' 13 | DISTR_STATUS_ENDPOINT: '{{ .statusEndpoint }}' 14 | DISTR_METRICS_ENDPOINT: '{{ .metricsEndpoint }}' 15 | DISTR_LOGS_ENDPOINT: '{{ .logsEndpoint }}' 16 | DISTR_INTERVAL: '{{ .agentInterval }}' 17 | DISTR_AGENT_VERSION_ID: '{{ .agentVersionId }}' 18 | DISTR_AGENT_SCRATCH_DIR: /scratch 19 | {{- if .registryEnabled }} 20 | DISTR_REGISTRY_HOST: '{{ .registryHost }}' 21 | DISTR_REGISTRY_PLAIN_HTTP: '{{ .registryPlainHttp }}' 22 | {{- end }} 23 | HOST_DOCKER_CONFIG_DIR: ${HOST_DOCKER_CONFIG_DIR-${HOME}/.docker} 24 | volumes: 25 | - /var/run/docker.sock:/var/run/docker.sock 26 | - scratch:/scratch 27 | - ${HOST_DOCKER_CONFIG_DIR-${HOME}/.docker}:/root/.docker:ro 28 | volumes: 29 | scratch: 30 | -------------------------------------------------------------------------------- /internal/resources/embedded/apps/hello-distr/template.env: -------------------------------------------------------------------------------- 1 | # this is a copy of https://github.com/glasskube/hello-distr/blob/0.1.9/deploy/env.template 2 | # mandatory values: 3 | HELLO_DISTR_HOST= # hostname only, do not include the scheme or a path 4 | HELLO_DISTR_DB_NAME= 5 | HELLO_DISTR_DB_USER= 6 | HELLO_DISTR_DB_PASSWORD= 7 | -------------------------------------------------------------------------------- /internal/security/password_test.go: -------------------------------------------------------------------------------- 1 | package security_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/glasskube/distr/internal/security" 7 | "github.com/glasskube/distr/internal/types" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestHashPassword(t *testing.T) { 12 | g := NewWithT(t) 13 | u1 := types.UserAccount{Password: "12345678"} 14 | u2 := types.UserAccount{Password: "12345678"} 15 | g.Expect(security.HashPassword(&u1)).NotTo(HaveOccurred()) 16 | g.Expect(u1.Password).To(BeEmpty()) 17 | g.Expect(u1.PasswordSalt).NotTo(BeEmpty()) 18 | g.Expect(security.HashPassword(&u2)).NotTo(HaveOccurred()) 19 | g.Expect(u2.Password).To(BeEmpty()) 20 | g.Expect(u2.PasswordSalt).NotTo(BeEmpty()) 21 | g.Expect(u1.PasswordSalt).NotTo(Equal(u2.PasswordSalt)) 22 | g.Expect(u1.PasswordHash).NotTo(Equal(u2.PasswordHash)) 23 | } 24 | 25 | func TestVerifyPassword(t *testing.T) { 26 | g := NewWithT(t) 27 | pw := "12345678" 28 | u := types.UserAccount{Password: pw} 29 | g.Expect(security.HashPassword(&u)).NotTo(HaveOccurred()) 30 | g.Expect(security.VerifyPassword(u, pw)).NotTo(HaveOccurred()) 31 | g.Expect(security.VerifyPassword(u, "wrong")).To(MatchError(security.ErrInvalidPassword)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/server/noop.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "context" 4 | 5 | type noopServer struct{} 6 | 7 | func (s *noopServer) Start(addr string) error { 8 | return nil 9 | } 10 | 11 | func (s *noopServer) Shutdown(ctx context.Context) { 12 | } 13 | 14 | func (s *noopServer) WaitForShutdown() { 15 | } 16 | 17 | func NewNoop() Server { 18 | return &noopServer{} 19 | } 20 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "context" 4 | 5 | type Server interface { 6 | Start(addr string) error 7 | Shutdown(ctx context.Context) 8 | WaitForShutdown() 9 | } 10 | -------------------------------------------------------------------------------- /internal/svc/options.go: -------------------------------------------------------------------------------- 1 | package svc 2 | 3 | type RegistryOption func(*Registry) 4 | 5 | func ExecDbMigration(migrate bool) RegistryOption { 6 | return func(reg *Registry) { 7 | reg.execDbMigrations = migrate 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /internal/types/access_token.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/glasskube/distr/internal/authkey" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type AccessToken struct { 11 | ID uuid.UUID `db:"id"` 12 | CreatedAt time.Time `db:"created_at"` 13 | ExpiresAt *time.Time `db:"expires_at"` 14 | LastUsedAt *time.Time `db:"last_used_at"` 15 | Label *string `db:"label"` 16 | Key authkey.Key `db:"key"` 17 | UserAccountID uuid.UUID `db:"user_account_id"` 18 | OrganizationID uuid.UUID `db:"organization_id"` 19 | } 20 | 21 | func (tok AccessToken) HasExpired() bool { 22 | return tok.ExpiresAt == nil || tok.ExpiresAt.After(time.Now()) 23 | } 24 | 25 | type AccessTokenWithUserAccount struct { 26 | AccessToken 27 | UserAccount UserAccount `db:"user_account"` 28 | UserRole UserRole `db:"user_role"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/types/agent_version.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/Masterminds/semver/v3" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | var minVersionMultiDeployment = semver.MustParse("1.6.0") 12 | 13 | type AgentVersion struct { 14 | ID uuid.UUID `db:"id" json:"id"` 15 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 16 | Name string `db:"name" json:"name"` 17 | ManifestFileRevision string `db:"manifest_file_revision" json:"-"` 18 | ComposeFileRevision string `db:"compose_file_revision" json:"-"` 19 | } 20 | 21 | func (av AgentVersion) CheckMultiDeploymentSupported() error { 22 | if av.Name == "snapshot" { 23 | return nil 24 | } 25 | sv, err := semver.NewVersion(av.Name) 26 | if err != nil { 27 | return err 28 | } 29 | if sv.LessThan(minVersionMultiDeployment) { 30 | return fmt.Errorf("multi deployments not supported by agent version %v (requires %v)", sv, minVersionMultiDeployment) 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/types/application.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Application struct { 10 | ID uuid.UUID `db:"id" json:"id"` 11 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 12 | OrganizationID uuid.UUID `db:"organization_id" json:"-"` 13 | Name string `db:"name" json:"name"` 14 | Type DeploymentType `db:"type" json:"type"` 15 | ImageID *uuid.UUID `db:"image_id" json:"-"` 16 | Versions []ApplicationVersion `db:"versions" json:"versions"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/types/artifact_license.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type ArtifactLicenseBase struct { 10 | ID uuid.UUID `db:"id" json:"id"` 11 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 12 | Name string `db:"name" json:"name"` 13 | ExpiresAt *time.Time `db:"expires_at" json:"expiresAt,omitempty"` 14 | OrganizationID uuid.UUID `db:"organization_id" json:"-"` 15 | OwnerUserAccountID *uuid.UUID `db:"owner_useraccount_id" json:"ownerUserAccountId,omitempty"` 16 | } 17 | 18 | type ArtifactLicenseSelection struct { 19 | ArtifactID uuid.UUID `db:"artifact_id" json:"artifactId"` 20 | VersionIDs []uuid.UUID `db:"versions" json:"versionIds"` 21 | } 22 | 23 | type ArtifactLicense struct { 24 | ArtifactLicenseBase 25 | Artifacts []ArtifactLicenseSelection `db:"artifacts" json:"artifacts,omitempty"` 26 | } 27 | -------------------------------------------------------------------------------- /internal/types/artifact_version.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type ArtifactVersion struct { 10 | ID uuid.UUID `db:"id" json:"id"` 11 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 12 | CreatedByUserAccountID *uuid.UUID `db:"created_by_useraccount_id" json:"-"` 13 | UpdatedAt *time.Time `db:"updated_at" json:"updatedAt"` 14 | UpdatedByUserAccountID *uuid.UUID `db:"updated_by_useraccount_id" json:"-"` 15 | Name string `db:"name" json:"name"` 16 | ManifestBlobDigest Digest `db:"manifest_blob_digest" json:"manifestBlobDigest"` 17 | ManifestBlobSize int64 `db:"manifest_blob_size" json:"-"` 18 | ManifestContentType string `db:"manifest_content_type" json:"manifestContentType"` 19 | ArtifactID uuid.UUID `db:"artifact_id" json:"artifactId"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/types/artifact_version_part.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type ArtifactVersionPart struct { 8 | ArtifactVersionID uuid.UUID `db:"artifact_version_id" json:"-"` 9 | ArtifactBlobDigest Digest `db:"artifact_blob_digest" json:"-"` 10 | ArtifactBlobSize int64 `db:"artifact_blob_size"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/types/artifact_version_pull.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | type ArtifactVersionPull struct { 6 | CreatedAt time.Time `json:"createdAt"` 7 | RemoteAddress *string `json:"remoteAddress,omitempty"` 8 | UserAccount *UserAccount `json:"userAccount,omitempty"` 9 | Artifact Artifact `json:"artifact"` 10 | ArtifactVersion ArtifactVersion `json:"artifactVersion"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/types/deployment_log_record.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type DeploymentLogRecord struct { 10 | ID uuid.UUID `db:"id"` 11 | CreatedAt time.Time `db:"created_at"` 12 | DeploymentID uuid.UUID `db:"deployment_id"` 13 | DeploymentRevisionID uuid.UUID `db:"deployment_revision_id"` 14 | Resource string `db:"resource"` 15 | Timestamp time.Time `db:"timestamp"` 16 | Severity string `db:"severity"` 17 | Body string `db:"body"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/types/deployment_revision.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/google/uuid" 4 | 5 | type DeploymentRevision struct { 6 | Base 7 | DeploymentID uuid.UUID `db:"deployment_id" json:"deploymentId"` 8 | ApplicationVersionID uuid.UUID `db:"application_version_id" json:"applicationVersionId"` 9 | ValuesYaml []byte `db:"-" json:"valuesYaml,omitempty"` 10 | EnvFileData []byte `db:"-" json:"-"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/types/deployment_revision_status.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type DeploymentRevisionStatus struct { 10 | ID uuid.UUID `db:"id" json:"id"` 11 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 12 | DeploymentRevisionID string `db:"deployment_revision_id" json:"deploymentRevisionId"` 13 | Type DeploymentStatusType `db:"type" json:"type"` 14 | Message string `db:"message" json:"message"` 15 | } 16 | -------------------------------------------------------------------------------- /internal/types/deployment_target_status.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type DeploymentTargetStatus struct { 10 | // unfortunately Base nested type doesn't work when ApplicationVersion is a nested row in an SQL query 11 | ID uuid.UUID `db:"id" json:"id"` 12 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 13 | Message string `db:"message" json:"message"` 14 | DeploymentTargetID uuid.UUID `db:"deployment_target_id" json:"-"` 15 | } 16 | -------------------------------------------------------------------------------- /internal/types/file.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type File struct { 10 | ID uuid.UUID `db:"id" json:"id"` 11 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 12 | OrganizationID *uuid.UUID `db:"organization_id" json:"-"` 13 | ContentType string `db:"content_type" json:"contentType"` 14 | Data []byte `db:"data" json:"data"` 15 | FileName string `db:"file_name" json:"fileName"` 16 | FileSize int64 `db:"file_size" json:"fileSize"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/types/tutorials.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | type TutorialProgressEvent struct { 6 | StepID string `json:"stepId"` 7 | TaskID string `json:"taskId"` 8 | Value any `json:"value,omitempty"` 9 | CreatedAt time.Time `json:"createdAt"` 10 | } 11 | 12 | type TutorialProgress struct { 13 | Tutorial Tutorial `db:"tutorial" json:"tutorial"` 14 | CreatedAt time.Time `db:"created_at" json:"createdAt"` 15 | Events []TutorialProgressEvent `db:"events" json:"events,omitempty"` 16 | CompletedAt *time.Time `db:"completed_at" json:"completedAt,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/util/maps_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/glasskube/distr/internal/util" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMergeIntoRecursive(t *testing.T) { 11 | g := NewWithT(t) 12 | a := map[string]any{ 13 | "foo": "bar", 14 | "number": 1, 15 | "only_a": map[string]any{"a": "a"}, 16 | "both": map[string]any{"aa": "aa"}, 17 | } 18 | b := map[string]any{ 19 | "foo": "hello", 20 | "only_b": map[string]any{"b": "b"}, 21 | "both": map[string]any{"slice": []any{1, 2, 3}}, 22 | } 23 | g.Expect(util.MergeIntoRecursive(a, b)).NotTo(HaveOccurred()) 24 | g.Expect(a).To(Equal(map[string]any{ 25 | "foo": "hello", 26 | "number": 1, 27 | "only_a": map[string]any{"a": "a"}, 28 | "only_b": map[string]any{"b": "b"}, 29 | "both": map[string]any{"aa": "aa", "slice": []any{1, 2, 3}}, 30 | })) 31 | } 32 | 33 | func TestMergeIntoRecursive_Error(t *testing.T) { 34 | g := NewWithT(t) 35 | a := map[string]any{ 36 | "foo": "bar", 37 | } 38 | b := map[string]any{ 39 | "foo": map[string]any{"b": "b"}, 40 | } 41 | g.Expect(util.MergeIntoRecursive(a, b)).To(HaveOccurred()) 42 | } 43 | -------------------------------------------------------------------------------- /internal/util/pointer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func PtrTo[T any](value T) *T { 4 | return &value 5 | } 6 | 7 | func PtrCopy[T any](ptr *T) *T { 8 | if ptr == nil { 9 | return nil 10 | } 11 | v := *ptr 12 | return &v 13 | } 14 | 15 | // PtrEq returns true iff both a and b are nil pointers or their dereferenced values are equal 16 | func PtrEq[T comparable](a, b *T) bool { 17 | return (a == nil && b == nil) || (a != nil && b != nil && *a == *b) 18 | } 19 | -------------------------------------------------------------------------------- /internal/util/pointer_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/glasskube/distr/internal/util" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestPtrEq(t *testing.T) { 11 | g := NewWithT(t) 12 | g.Expect(util.PtrEq[any](nil, nil)).To(BeTrue()) 13 | g.Expect(util.PtrEq(util.PtrTo("a"), nil)).To(BeFalse()) 14 | g.Expect(util.PtrEq(nil, util.PtrTo("b"))).To(BeFalse()) 15 | g.Expect(util.PtrEq(util.PtrTo("a"), util.PtrTo("b"))).To(BeFalse()) 16 | g.Expect(util.PtrEq(util.PtrTo("a"), util.PtrTo("a"))).To(BeTrue()) 17 | } 18 | -------------------------------------------------------------------------------- /internal/util/require.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func Must(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | 9 | func Require[T any](v T, err error) T { 10 | Must(err) 11 | return v 12 | } 13 | -------------------------------------------------------------------------------- /internal/validation/error.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ErrValidationFailed = errors.New("validation failed") 9 | 10 | func NewValidationFailedError(reason string) error { 11 | return fmt.Errorf("%w: %v", ErrValidationFailed, reason) 12 | } 13 | -------------------------------------------------------------------------------- /internal/validation/password.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | func ValidatePassword(password string) error { 4 | if len(password) < 8 { 5 | return NewValidationFailedError("password is too short (minimum 8 characters are required)") 6 | } 7 | return nil 8 | } 9 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = "24" 3 | go = "1.24.3" 4 | golangci-lint = "2.1.6" 5 | helm-docs = "1.14.2" 6 | -------------------------------------------------------------------------------- /sdk/js/README.md: -------------------------------------------------------------------------------- 1 | # Distr SDK 2 | 3 | You can install the Distr SDK for JavaScript from [npmjs.org](https://npmjs.org/): 4 | 5 | ```shell 6 | npm install --save @glasskube/distr-sdk 7 | ``` 8 | 9 | Conceptually, the SDK is divided into two parts: 10 | 11 | - A high-level service called `DistrService`, which provides a simplified interface for interacting with the Distr API. 12 | - A low-level client called `Client`, which provides a more direct interface for interacting with the Distr API. 13 | 14 | In order to connect to the Distr API, you have to create a Personal Access Token (PAT) in the Distr web interface. 15 | Optionally, you can specify the URL of the Distr API you want to communicate with. It defaults to `https://app.distr.sh/api/v1`. 16 | 17 | ```typescript 18 | import {DistrService} from '@glasskube/distr-sdk'; 19 | const service = new DistrService({ 20 | // to use your selfhosted instance, set apiBase: 'https://selfhosted-instance.company/api/v1', 21 | apiKey: '', 22 | }); 23 | // do something with the service 24 | ``` 25 | 26 | The [src/examples](https://github.com/glasskube/distr/tree/main/sdk/js/src/examples) directory contains examples of how to use the SDK. 27 | 28 | See the [docs](https://github.com/glasskube/distr/tree/main/sdk/js/docs/README.md) for more information. 29 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/AccessToken.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / AccessToken 6 | 7 | # Interface: AccessToken 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Extended by 14 | 15 | - [`AccessTokenWithKey`](AccessTokenWithKey.md) 16 | 17 | ## Properties 18 | 19 | ### createdAt? 20 | 21 | > `optional` **createdAt**: `string` 22 | 23 | #### Inherited from 24 | 25 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 26 | 27 | --- 28 | 29 | ### expiresAt? 30 | 31 | > `optional` **expiresAt**: `string` 32 | 33 | --- 34 | 35 | ### id? 36 | 37 | > `optional` **id**: `string` 38 | 39 | #### Inherited from 40 | 41 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 42 | 43 | --- 44 | 45 | ### label? 46 | 47 | > `optional` **label**: `string` 48 | 49 | --- 50 | 51 | ### lastUsedAt? 52 | 53 | > `optional` **lastUsedAt**: `string` 54 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/AccessTokenWithKey.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / AccessTokenWithKey 6 | 7 | # Interface: AccessTokenWithKey 8 | 9 | ## Extends 10 | 11 | - [`AccessToken`](AccessToken.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`AccessToken`](AccessToken.md).[`createdAt`](AccessToken.md#createdat) 22 | 23 | --- 24 | 25 | ### expiresAt? 26 | 27 | > `optional` **expiresAt**: `string` 28 | 29 | #### Inherited from 30 | 31 | [`AccessToken`](AccessToken.md).[`expiresAt`](AccessToken.md#expiresat) 32 | 33 | --- 34 | 35 | ### id? 36 | 37 | > `optional` **id**: `string` 38 | 39 | #### Inherited from 40 | 41 | [`AccessToken`](AccessToken.md).[`id`](AccessToken.md#id) 42 | 43 | --- 44 | 45 | ### key 46 | 47 | > **key**: `string` 48 | 49 | --- 50 | 51 | ### label? 52 | 53 | > `optional` **label**: `string` 54 | 55 | #### Inherited from 56 | 57 | [`AccessToken`](AccessToken.md).[`label`](AccessToken.md#label) 58 | 59 | --- 60 | 61 | ### lastUsedAt? 62 | 63 | > `optional` **lastUsedAt**: `string` 64 | 65 | #### Inherited from 66 | 67 | [`AccessToken`](AccessToken.md).[`lastUsedAt`](AccessToken.md#lastusedat) 68 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/AgentVersion.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / AgentVersion 6 | 7 | # Interface: AgentVersion 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 22 | 23 | --- 24 | 25 | ### id? 26 | 27 | > `optional` **id**: `string` 28 | 29 | #### Inherited from 30 | 31 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 32 | 33 | --- 34 | 35 | ### name 36 | 37 | > **name**: `string` 38 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/Application.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / Application 6 | 7 | # Interface: Application 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md).[`Named`](Named.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 22 | 23 | --- 24 | 25 | ### id? 26 | 27 | > `optional` **id**: `string` 28 | 29 | #### Inherited from 30 | 31 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 32 | 33 | --- 34 | 35 | ### imageUrl? 36 | 37 | > `optional` **imageUrl**: `string` 38 | 39 | --- 40 | 41 | ### name? 42 | 43 | > `optional` **name**: `string` 44 | 45 | #### Inherited from 46 | 47 | [`Named`](Named.md).[`name`](Named.md#name) 48 | 49 | --- 50 | 51 | ### type 52 | 53 | > **type**: [`DeploymentType`](../type-aliases/DeploymentType.md) 54 | 55 | --- 56 | 57 | ### versions? 58 | 59 | > `optional` **versions**: [`ApplicationVersion`](ApplicationVersion.md)[] 60 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/ApplicationVersion.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / ApplicationVersion 6 | 7 | # Interface: ApplicationVersion 8 | 9 | ## Properties 10 | 11 | ### applicationId? 12 | 13 | > `optional` **applicationId**: `string` 14 | 15 | --- 16 | 17 | ### archivedAt? 18 | 19 | > `optional` **archivedAt**: `string` 20 | 21 | --- 22 | 23 | ### chartName? 24 | 25 | > `optional` **chartName**: `string` 26 | 27 | --- 28 | 29 | ### chartType? 30 | 31 | > `optional` **chartType**: [`HelmChartType`](../type-aliases/HelmChartType.md) 32 | 33 | --- 34 | 35 | ### chartUrl? 36 | 37 | > `optional` **chartUrl**: `string` 38 | 39 | --- 40 | 41 | ### chartVersion? 42 | 43 | > `optional` **chartVersion**: `string` 44 | 45 | --- 46 | 47 | ### createdAt? 48 | 49 | > `optional` **createdAt**: `string` 50 | 51 | --- 52 | 53 | ### id? 54 | 55 | > `optional` **id**: `string` 56 | 57 | --- 58 | 59 | ### name 60 | 61 | > **name**: `string` 62 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/BaseModel.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / BaseModel 6 | 7 | # Interface: BaseModel 8 | 9 | ## Extended by 10 | 11 | - [`AccessToken`](AccessToken.md) 12 | - [`AgentVersion`](AgentVersion.md) 13 | - [`Application`](Application.md) 14 | - [`Deployment`](Deployment.md) 15 | - [`DeploymentRevisionStatus`](DeploymentRevisionStatus.md) 16 | - [`DeploymentTarget`](DeploymentTarget.md) 17 | - [`DeploymentTargetStatus`](DeploymentTargetStatus.md) 18 | - [`OrganizationBranding`](OrganizationBranding.md) 19 | - [`UserAccount`](UserAccount.md) 20 | 21 | ## Properties 22 | 23 | ### createdAt? 24 | 25 | > `optional` **createdAt**: `string` 26 | 27 | --- 28 | 29 | ### id? 30 | 31 | > `optional` **id**: `string` 32 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/CreateAccessTokenRequest.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / CreateAccessTokenRequest 6 | 7 | # Interface: CreateAccessTokenRequest 8 | 9 | ## Properties 10 | 11 | ### expiresAt? 12 | 13 | > `optional` **expiresAt**: `Date` 14 | 15 | --- 16 | 17 | ### label? 18 | 19 | > `optional` **label**: `string` 20 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/Deployment.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / Deployment 6 | 7 | # Interface: Deployment 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Extended by 14 | 15 | - [`DeploymentWithLatestRevision`](DeploymentWithLatestRevision.md) 16 | 17 | ## Properties 18 | 19 | ### createdAt? 20 | 21 | > `optional` **createdAt**: `string` 22 | 23 | #### Inherited from 24 | 25 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 26 | 27 | --- 28 | 29 | ### deploymentTargetId 30 | 31 | > **deploymentTargetId**: `string` 32 | 33 | --- 34 | 35 | ### dockerType? 36 | 37 | > `optional` **dockerType**: [`DockerType`](../type-aliases/DockerType.md) 38 | 39 | --- 40 | 41 | ### id? 42 | 43 | > `optional` **id**: `string` 44 | 45 | #### Inherited from 46 | 47 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 48 | 49 | --- 50 | 51 | ### logsEnabled 52 | 53 | > **logsEnabled**: `boolean` 54 | 55 | --- 56 | 57 | ### releaseName? 58 | 59 | > `optional` **releaseName**: `string` 60 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/DeploymentRequest.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentRequest 6 | 7 | # Interface: DeploymentRequest 8 | 9 | ## Properties 10 | 11 | ### applicationLicenseId? 12 | 13 | > `optional` **applicationLicenseId**: `string` 14 | 15 | --- 16 | 17 | ### applicationVersionId 18 | 19 | > **applicationVersionId**: `string` 20 | 21 | --- 22 | 23 | ### deploymentId? 24 | 25 | > `optional` **deploymentId**: `string` 26 | 27 | --- 28 | 29 | ### deploymentTargetId 30 | 31 | > **deploymentTargetId**: `string` 32 | 33 | --- 34 | 35 | ### dockerType? 36 | 37 | > `optional` **dockerType**: [`DockerType`](../type-aliases/DockerType.md) 38 | 39 | --- 40 | 41 | ### envFileData? 42 | 43 | > `optional` **envFileData**: `string` 44 | 45 | --- 46 | 47 | ### releaseName? 48 | 49 | > `optional` **releaseName**: `string` 50 | 51 | --- 52 | 53 | ### valuesYaml? 54 | 55 | > `optional` **valuesYaml**: `string` 56 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/DeploymentRevisionStatus.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentRevisionStatus 6 | 7 | # Interface: DeploymentRevisionStatus 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 22 | 23 | --- 24 | 25 | ### id? 26 | 27 | > `optional` **id**: `string` 28 | 29 | #### Inherited from 30 | 31 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 32 | 33 | --- 34 | 35 | ### message 36 | 37 | > **message**: `string` 38 | 39 | --- 40 | 41 | ### type 42 | 43 | > **type**: [`DeploymentStatusType`](../type-aliases/DeploymentStatusType.md) 44 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/DeploymentTargetAccessResponse.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentTargetAccessResponse 6 | 7 | # Interface: DeploymentTargetAccessResponse 8 | 9 | ## Properties 10 | 11 | ### connectUrl 12 | 13 | > **connectUrl**: `string` 14 | 15 | --- 16 | 17 | ### targetId 18 | 19 | > **targetId**: `string` 20 | 21 | --- 22 | 23 | ### targetSecret 24 | 25 | > **targetSecret**: `string` 26 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/DeploymentTargetStatus.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentTargetStatus 6 | 7 | # Interface: DeploymentTargetStatus 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 22 | 23 | --- 24 | 25 | ### id? 26 | 27 | > `optional` **id**: `string` 28 | 29 | #### Inherited from 30 | 31 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 32 | 33 | --- 34 | 35 | ### message 36 | 37 | > **message**: `string` 38 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/Named.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / Named 6 | 7 | # Interface: Named 8 | 9 | ## Extended by 10 | 11 | - [`Application`](Application.md) 12 | - [`DeploymentTarget`](DeploymentTarget.md) 13 | 14 | ## Properties 15 | 16 | ### name? 17 | 18 | > `optional` **name**: `string` 19 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/OrganizationBranding.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / OrganizationBranding 6 | 7 | # Interface: OrganizationBranding 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 22 | 23 | --- 24 | 25 | ### description? 26 | 27 | > `optional` **description**: `string` 28 | 29 | --- 30 | 31 | ### id? 32 | 33 | > `optional` **id**: `string` 34 | 35 | #### Inherited from 36 | 37 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 38 | 39 | --- 40 | 41 | ### logo? 42 | 43 | > `optional` **logo**: `string` 44 | 45 | --- 46 | 47 | ### logoContentType? 48 | 49 | > `optional` **logoContentType**: `string` 50 | 51 | --- 52 | 53 | ### logoFileName? 54 | 55 | > `optional` **logoFileName**: `string` 56 | 57 | --- 58 | 59 | ### title? 60 | 61 | > `optional` **title**: `string` 62 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/PatchApplicationRequest.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / PatchApplicationRequest 6 | 7 | # Interface: PatchApplicationRequest 8 | 9 | ## Properties 10 | 11 | ### name? 12 | 13 | > `optional` **name**: `string` 14 | 15 | --- 16 | 17 | ### versions? 18 | 19 | > `optional` **versions**: `object`[] 20 | 21 | #### archivedAt? 22 | 23 | > `optional` **archivedAt**: `string` 24 | 25 | #### id 26 | 27 | > **id**: `string` 28 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/PatchDeploymentRequest.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / PatchDeploymentRequest 6 | 7 | # Interface: PatchDeploymentRequest 8 | 9 | ## Properties 10 | 11 | ### logsEnabled? 12 | 13 | > `optional` **logsEnabled**: `boolean` 14 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/TokenResponse.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / TokenResponse 6 | 7 | # Interface: TokenResponse 8 | 9 | ## Properties 10 | 11 | ### token 12 | 13 | > **token**: `string` 14 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/UserAccount.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / UserAccount 6 | 7 | # Interface: UserAccount 8 | 9 | ## Extends 10 | 11 | - [`BaseModel`](BaseModel.md) 12 | 13 | ## Extended by 14 | 15 | - [`UserAccountWithRole`](UserAccountWithRole.md) 16 | 17 | ## Properties 18 | 19 | ### createdAt? 20 | 21 | > `optional` **createdAt**: `string` 22 | 23 | #### Inherited from 24 | 25 | [`BaseModel`](BaseModel.md).[`createdAt`](BaseModel.md#createdat) 26 | 27 | --- 28 | 29 | ### email 30 | 31 | > **email**: `string` 32 | 33 | --- 34 | 35 | ### id? 36 | 37 | > `optional` **id**: `string` 38 | 39 | #### Inherited from 40 | 41 | [`BaseModel`](BaseModel.md).[`id`](BaseModel.md#id) 42 | 43 | --- 44 | 45 | ### imageUrl? 46 | 47 | > `optional` **imageUrl**: `string` 48 | 49 | --- 50 | 51 | ### name? 52 | 53 | > `optional` **name**: `string` 54 | -------------------------------------------------------------------------------- /sdk/js/docs/interfaces/UserAccountWithRole.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / UserAccountWithRole 6 | 7 | # Interface: UserAccountWithRole 8 | 9 | ## Extends 10 | 11 | - [`UserAccount`](UserAccount.md) 12 | 13 | ## Properties 14 | 15 | ### createdAt? 16 | 17 | > `optional` **createdAt**: `string` 18 | 19 | #### Inherited from 20 | 21 | [`UserAccount`](UserAccount.md).[`createdAt`](UserAccount.md#createdat) 22 | 23 | --- 24 | 25 | ### email 26 | 27 | > **email**: `string` 28 | 29 | #### Inherited from 30 | 31 | [`UserAccount`](UserAccount.md).[`email`](UserAccount.md#email) 32 | 33 | --- 34 | 35 | ### id? 36 | 37 | > `optional` **id**: `string` 38 | 39 | #### Inherited from 40 | 41 | [`UserAccount`](UserAccount.md).[`id`](UserAccount.md#id) 42 | 43 | --- 44 | 45 | ### imageUrl? 46 | 47 | > `optional` **imageUrl**: `string` 48 | 49 | #### Inherited from 50 | 51 | [`UserAccount`](UserAccount.md).[`imageUrl`](UserAccount.md#imageurl) 52 | 53 | --- 54 | 55 | ### joinedOrgAt 56 | 57 | > **joinedOrgAt**: `string` 58 | 59 | --- 60 | 61 | ### name? 62 | 63 | > `optional` **name**: `string` 64 | 65 | #### Inherited from 66 | 67 | [`UserAccount`](UserAccount.md).[`name`](UserAccount.md#name) 68 | 69 | --- 70 | 71 | ### userRole 72 | 73 | > **userRole**: [`UserRole`](../type-aliases/UserRole.md) 74 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/ApplicationVersionFiles.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / ApplicationVersionFiles 6 | 7 | # Type Alias: ApplicationVersionFiles 8 | 9 | > **ApplicationVersionFiles** = `object` 10 | 11 | ## Properties 12 | 13 | ### baseValuesFile? 14 | 15 | > `optional` **baseValuesFile**: `string` 16 | 17 | --- 18 | 19 | ### composeFile? 20 | 21 | > `optional` **composeFile**: `string` 22 | 23 | --- 24 | 25 | ### templateFile? 26 | 27 | > `optional` **templateFile**: `string` 28 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/ClientConfig.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / ClientConfig 6 | 7 | # Type Alias: ClientConfig 8 | 9 | > **ClientConfig** = `object` 10 | 11 | ## Properties 12 | 13 | ### apiBase 14 | 15 | > **apiBase**: `string` 16 | 17 | The base URL of the Distr API ending with /api/v1, e.g. https://app.distr.sh/api/v1. 18 | 19 | --- 20 | 21 | ### apiKey 22 | 23 | > **apiKey**: `string` 24 | 25 | The API key to authenticate with the Distr API. 26 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/CreateDeploymentParams.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / CreateDeploymentParams 6 | 7 | # Type Alias: CreateDeploymentParams 8 | 9 | > **CreateDeploymentParams** = `object` 10 | 11 | ## Properties 12 | 13 | ### application 14 | 15 | > **application**: `object` 16 | 17 | #### id? 18 | 19 | > `optional` **id**: `string` 20 | 21 | #### versionId? 22 | 23 | > `optional` **versionId**: `string` 24 | 25 | --- 26 | 27 | ### kubernetesDeployment? 28 | 29 | > `optional` **kubernetesDeployment**: `object` 30 | 31 | #### releaseName 32 | 33 | > **releaseName**: `string` 34 | 35 | #### valuesYaml? 36 | 37 | > `optional` **valuesYaml**: `string` 38 | 39 | --- 40 | 41 | ### target 42 | 43 | > **target**: `object` 44 | 45 | #### kubernetes? 46 | 47 | > `optional` **kubernetes**: `object` 48 | 49 | ##### kubernetes.namespace 50 | 51 | > **namespace**: `string` 52 | 53 | ##### kubernetes.scope 54 | 55 | > **scope**: [`DeploymentTargetScope`](DeploymentTargetScope.md) 56 | 57 | #### name 58 | 59 | > **name**: `string` 60 | 61 | #### type 62 | 63 | > **type**: [`DeploymentType`](DeploymentType.md) 64 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/CreateDeploymentResult.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / CreateDeploymentResult 6 | 7 | # Type Alias: CreateDeploymentResult 8 | 9 | > **CreateDeploymentResult** = `object` 10 | 11 | ## Properties 12 | 13 | ### access 14 | 15 | > **access**: [`DeploymentTargetAccessResponse`](../interfaces/DeploymentTargetAccessResponse.md) 16 | 17 | --- 18 | 19 | ### deploymentTarget 20 | 21 | > **deploymentTarget**: [`DeploymentTarget`](../interfaces/DeploymentTarget.md) 22 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/DeploymentStatusType.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentStatusType 6 | 7 | # Type Alias: DeploymentStatusType 8 | 9 | > **DeploymentStatusType** = `"ok"` \| `"progressing"` \| `"error"` 10 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/DeploymentTargetScope.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentTargetScope 6 | 7 | # Type Alias: DeploymentTargetScope 8 | 9 | > **DeploymentTargetScope** = `"cluster"` \| `"namespace"` 10 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/DeploymentType.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DeploymentType 6 | 7 | # Type Alias: DeploymentType 8 | 9 | > **DeploymentType** = `"docker"` \| `"kubernetes"` 10 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/DockerType.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / DockerType 6 | 7 | # Type Alias: DockerType 8 | 9 | > **DockerType** = `"compose"` \| `"swarm"` 10 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/HelmChartType.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / HelmChartType 6 | 7 | # Type Alias: HelmChartType 8 | 9 | > **HelmChartType** = `"repository"` \| `"oci"` 10 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/IsOutdatedResult.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / IsOutdatedResult 6 | 7 | # Type Alias: IsOutdatedResult 8 | 9 | > **IsOutdatedResult** = `object` 10 | 11 | ## Properties 12 | 13 | ### application 14 | 15 | > **application**: [`Application`](../interfaces/Application.md) 16 | 17 | --- 18 | 19 | ### deploymentTarget 20 | 21 | > **deploymentTarget**: [`DeploymentTarget`](../interfaces/DeploymentTarget.md) 22 | 23 | --- 24 | 25 | ### newerVersions 26 | 27 | > **newerVersions**: [`ApplicationVersion`](../interfaces/ApplicationVersion.md)[] 28 | 29 | --- 30 | 31 | ### outdated 32 | 33 | > **outdated**: `boolean` 34 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/LatestVersionStrategy.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / LatestVersionStrategy 6 | 7 | # Type Alias: LatestVersionStrategy 8 | 9 | > **LatestVersionStrategy** = `"semver"` \| `"chronological"` 10 | 11 | The strategy for determining the latest version of an application. 12 | 13 | - 'semver' uses semantic versioning to determine the latest version. 14 | - 'chronological' uses the creation date of the versions to determine the latest version. 15 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/UpdateDeploymentParams.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / UpdateDeploymentParams 6 | 7 | # Type Alias: UpdateDeploymentParams 8 | 9 | > **UpdateDeploymentParams** = `object` 10 | 11 | ## Properties 12 | 13 | ### applicationVersionId? 14 | 15 | > `optional` **applicationVersionId**: `string` 16 | 17 | --- 18 | 19 | ### deploymentTargetId 20 | 21 | > **deploymentTargetId**: `string` 22 | 23 | --- 24 | 25 | ### kubernetesDeployment? 26 | 27 | > `optional` **kubernetesDeployment**: `object` 28 | 29 | #### valuesYaml? 30 | 31 | > `optional` **valuesYaml**: `string` 32 | -------------------------------------------------------------------------------- /sdk/js/docs/type-aliases/UserRole.md: -------------------------------------------------------------------------------- 1 | [**@glasskube/distr-sdk**](../README.md) 2 | 3 | --- 4 | 5 | [@glasskube/distr-sdk](../README.md) / UserRole 6 | 7 | # Type Alias: UserRole 8 | 9 | > **UserRole** = `"vendor"` \| `"customer"` 10 | -------------------------------------------------------------------------------- /sdk/js/jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"] 3 | } 4 | -------------------------------------------------------------------------------- /sdk/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@glasskube/distr-sdk", 3 | "version": "1.11.1", 4 | "type": "module", 5 | "license": "Apache-2.0", 6 | "description": "Distr SDK", 7 | "homepage": "https://distr.sh", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/glasskube/distr.git", 11 | "directory": "sdk/js" 12 | }, 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "scripts": { 16 | "build": "rimraf dist && tsc", 17 | "docs": "typedoc" 18 | }, 19 | "files": [ 20 | "dist/" 21 | ], 22 | "dependencies": { 23 | "semver": "^7.6.3" 24 | }, 25 | "devDependencies": { 26 | "@types/semver": "^7.5.8", 27 | "prettier": "^3.5.3", 28 | "rimraf": "^6.0.1", 29 | "typedoc": "^0.28.0", 30 | "typedoc-plugin-markdown": "^4.5.0", 31 | "typescript": "^5.7.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sdk/js/src/client/config.ts: -------------------------------------------------------------------------------- 1 | export const defaultClientConfig = {apiBase: 'https://app.distr.sh/api/v1/'}; 2 | 3 | export type ConditionalPartial = Omit & Partial>; 4 | -------------------------------------------------------------------------------- /sdk/js/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './service'; 3 | -------------------------------------------------------------------------------- /sdk/js/src/examples/check-deployment-outdated.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | 6 | const deploymentTargetId = ''; 7 | const outdatedRes = await gc.isOutdated(deploymentTargetId); 8 | console.log(outdatedRes); 9 | -------------------------------------------------------------------------------- /sdk/js/src/examples/config.ts: -------------------------------------------------------------------------------- 1 | export const clientConfig = { 2 | // to use your selfhosted instance, set apiBase: 'https://selfhosted-instance.company/api/v1', 3 | apiKey: '', 4 | }; 5 | -------------------------------------------------------------------------------- /sdk/js/src/examples/create-deployment-docker.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | 6 | const appId = ''; 7 | const result = await gc.createDeployment({ 8 | target: { 9 | name: 'test-docker-deployment', 10 | type: 'docker', 11 | }, 12 | application: { 13 | id: appId, 14 | }, 15 | }); 16 | console.log(result); 17 | -------------------------------------------------------------------------------- /sdk/js/src/examples/create-deployment-kubernetes.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | 6 | const appId = ''; 7 | const result = await gc.createDeployment({ 8 | target: { 9 | name: 'test-kubernetes-deployment', 10 | type: 'kubernetes', 11 | kubernetes: { 12 | namespace: 'my-namespace', 13 | scope: 'namespace', 14 | }, 15 | }, 16 | application: { 17 | id: appId, 18 | }, 19 | kubernetesDeployment: { 20 | releaseName: 'my-release', 21 | valuesYaml: 'my-values: true', 22 | }, 23 | }); 24 | console.log(result); 25 | -------------------------------------------------------------------------------- /sdk/js/src/examples/create-version-docker.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | const appId = 'd91ede4e-f909-4f72-a416-f6f5f797682f'; 6 | 7 | const composeFile = ` 8 | services: 9 | my-postgres: 10 | image: 'postgres:17.2-alpine3.20' 11 | ports: 12 | - '5434:5432' 13 | environment: 14 | POSTGRES_USER: \${POSTGRES_USER} 15 | POSTGRES_PASSWORD: \${POSTGRES_PASSWORD} 16 | POSTGRES_DB: \${POSTGRES_DB} 17 | volumes: 18 | - 'postgres-data:/var/lib/postgresql/data/' 19 | 20 | volumes: 21 | postgres-data: 22 | `; 23 | 24 | const templateFile = ` 25 | POSTGRES_USER=some-user # REPLACE THIS 26 | POSTGRES_PASSWORD=some-password # REPLACE THIS 27 | POSTGRES_DB=some-db # REPLACE THIS`; 28 | 29 | const newDockerVersion = await gc.createDockerApplicationVersion(appId, '17.2-alpine3.20+2', { 30 | composeFile, 31 | templateFile, 32 | }); 33 | console.log(`* created new version ${newDockerVersion.name} (id: ${newDockerVersion.id}) for docker app ${appId}`); 34 | -------------------------------------------------------------------------------- /sdk/js/src/examples/create-version-kubernetes.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | const kubernetesAppId = ''; 6 | const newKubernetesVersion = await gc.createKubernetesApplicationVersion(kubernetesAppId, 'v1.0.1', { 7 | chartName: 'my-chart', 8 | chartVersion: '1.0.1', 9 | chartType: 'repository', 10 | chartUrl: 'https://example.com/my-chart-1.0.1.tgz', 11 | baseValuesFile: 'base: values', 12 | templateFile: 'template: true', 13 | }); 14 | console.log( 15 | `* created new version ${newKubernetesVersion.name} (id: ${newKubernetesVersion.id}) for kubernetes app ${kubernetesAppId}` 16 | ); 17 | -------------------------------------------------------------------------------- /sdk/js/src/examples/update-deployment-docker.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | 6 | const deploymentTargetId = ''; 7 | await gc.updateDeployment({deploymentTargetId}); // update to latest version (according to the given strategy) of application that is already deployed 8 | -------------------------------------------------------------------------------- /sdk/js/src/examples/update-deployment-kubernetes.ts: -------------------------------------------------------------------------------- 1 | import {DistrService} from '../client/service'; 2 | import {clientConfig} from './config'; 3 | 4 | const gc = new DistrService(clientConfig); 5 | 6 | const deploymentTargetId = ''; 7 | await gc.updateDeployment({deploymentTargetId, kubernetesDeployment: {valuesYaml: 'new: values'}}); // update to latest version (according to the given strategy) of application that is already deployed 8 | -------------------------------------------------------------------------------- /sdk/js/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /sdk/js/src/types/access-token.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel} from './base'; 2 | 3 | export interface AccessToken extends BaseModel { 4 | expiresAt?: string; 5 | lastUsedAt?: string; 6 | label?: string; 7 | } 8 | 9 | export interface AccessTokenWithKey extends AccessToken { 10 | key: string; 11 | } 12 | 13 | export interface CreateAccessTokenRequest { 14 | label?: string; 15 | expiresAt?: Date; 16 | } 17 | -------------------------------------------------------------------------------- /sdk/js/src/types/agent-version.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel} from './base'; 2 | 3 | export interface AgentVersion extends BaseModel { 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /sdk/js/src/types/application.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel, Named} from './base'; 2 | import {DeploymentType, HelmChartType, DockerType} from './deployment'; 3 | 4 | export interface Application extends BaseModel, Named { 5 | type: DeploymentType; 6 | imageUrl?: string; 7 | versions?: ApplicationVersion[]; 8 | } 9 | 10 | export interface ApplicationVersion { 11 | id?: string; 12 | name: string; 13 | createdAt?: string; 14 | archivedAt?: string; 15 | applicationId?: string; 16 | chartType?: HelmChartType; 17 | chartName?: string; 18 | chartUrl?: string; 19 | chartVersion?: string; 20 | } 21 | 22 | export interface PatchApplicationRequest { 23 | name?: string; 24 | versions?: {id: string; archivedAt?: string}[]; 25 | } 26 | -------------------------------------------------------------------------------- /sdk/js/src/types/base.ts: -------------------------------------------------------------------------------- 1 | export interface BaseModel { 2 | id?: string; 3 | createdAt?: string; 4 | } 5 | 6 | export interface Named { 7 | name?: string; 8 | } 9 | 10 | export interface TokenResponse { 11 | token: string; 12 | } 13 | 14 | export interface DeploymentTargetAccessResponse { 15 | connectUrl: string; 16 | targetId: string; 17 | targetSecret: string; 18 | } 19 | -------------------------------------------------------------------------------- /sdk/js/src/types/deployment-target.ts: -------------------------------------------------------------------------------- 1 | import {AgentVersion} from './agent-version'; 2 | import {BaseModel, Named} from './base'; 3 | import {DeploymentTargetScope, DeploymentType, DeploymentWithLatestRevision} from './deployment'; 4 | import {UserAccountWithRole} from './user-account'; 5 | 6 | export interface DeploymentTarget extends BaseModel, Named { 7 | name: string; 8 | type: DeploymentType; 9 | namespace?: string; 10 | scope?: DeploymentTargetScope; 11 | createdBy?: UserAccountWithRole; 12 | currentStatus?: DeploymentTargetStatus; 13 | /** 14 | * @deprecated This property will be removed in v2. Please consider using `deployments` instead. 15 | */ 16 | deployment?: DeploymentWithLatestRevision; 17 | deployments: DeploymentWithLatestRevision[]; 18 | agentVersion?: AgentVersion; 19 | reportedAgentVersionId?: string; 20 | metricsEnabled: boolean; 21 | } 22 | 23 | export interface DeploymentTargetStatus extends BaseModel { 24 | message: string; 25 | } 26 | -------------------------------------------------------------------------------- /sdk/js/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access-token'; 2 | export * from './agent-version'; 3 | export * from './application'; 4 | export * from './base'; 5 | export * from './deployment'; 6 | export * from './deployment-target'; 7 | export * from './organization-branding'; 8 | export * from './user-account'; 9 | -------------------------------------------------------------------------------- /sdk/js/src/types/organization-branding.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel} from './base'; 2 | 3 | export interface OrganizationBranding extends BaseModel { 4 | title?: string; 5 | description?: string; 6 | logo?: string; 7 | logoFileName?: string; 8 | logoContentType?: string; 9 | } 10 | -------------------------------------------------------------------------------- /sdk/js/src/types/user-account.ts: -------------------------------------------------------------------------------- 1 | import {BaseModel} from './base'; 2 | 3 | export type UserRole = 'vendor' | 'customer'; 4 | 5 | export interface UserAccount extends BaseModel { 6 | email: string; 7 | name?: string; 8 | imageUrl?: string; 9 | } 10 | 11 | export interface UserAccountWithRole extends UserAccount { 12 | userRole: UserRole; 13 | joinedOrgAt: string; 14 | } 15 | -------------------------------------------------------------------------------- /sdk/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "declaration": true 9 | }, 10 | "exclude": [ 11 | "src/examples" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /sdk/js/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./src/index.ts"], 3 | "plugin": ["typedoc-plugin-markdown"], 4 | "out": "./docs", 5 | "readme": "none", 6 | "typeDeclarationVisibility": "verbose", 7 | "formatWithPrettier": true, 8 | "prettierConfigFile": "../../.prettierrc.json", 9 | "disableSources": true 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | --------------------------------------------------------------------------------