| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- package porter_app
- import (
- "context"
- "encoding/base64"
- "net/http"
- "sort"
- "strings"
- "github.com/porter-dev/porter/internal/kubernetes"
- "github.com/google/uuid"
- "github.com/porter-dev/api-contracts/generated/go/helpers"
- "github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
- "github.com/porter-dev/porter/internal/deployment_target"
- "github.com/porter-dev/porter/internal/repository"
- "github.com/porter-dev/porter/internal/porter_app"
- "connectrpc.com/connect"
- porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
- "gopkg.in/yaml.v2"
- v2 "github.com/porter-dev/porter/internal/porter_app/v2"
- "github.com/porter-dev/porter/internal/telemetry"
- "github.com/porter-dev/porter/api/server/authz"
- "github.com/porter-dev/porter/api/server/handlers"
- "github.com/porter-dev/porter/api/server/shared"
- "github.com/porter-dev/porter/api/server/shared/apierrors"
- "github.com/porter-dev/porter/api/server/shared/config"
- "github.com/porter-dev/porter/api/server/shared/requestutils"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/models"
- )
- // PorterYAMLFromRevisionHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
- type PorterYAMLFromRevisionHandler struct {
- handlers.PorterHandlerReadWriter
- authz.KubernetesAgentGetter
- }
- // NewPorterYAMLFromRevisionHandler returns a new PorterYAMLFromRevisionHandler
- func NewPorterYAMLFromRevisionHandler(
- config *config.Config,
- decoderValidator shared.RequestDecoderValidator,
- writer shared.ResultWriter,
- ) *PorterYAMLFromRevisionHandler {
- return &PorterYAMLFromRevisionHandler{
- PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
- KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
- }
- }
- // PorterYAMLFromRevisionRequest is the request object for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
- type PorterYAMLFromRevisionRequest struct {
- ShouldFormatForExport bool `schema:"should_format_for_export"`
- }
- // PorterYAMLFromRevisionResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
- type PorterYAMLFromRevisionResponse struct {
- B64PorterYAML string `json:"b64_porter_yaml"`
- }
- // ServeHTTP takes a porter app revision and returns the porter yaml for it
- func (c *PorterYAMLFromRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ctx, span := telemetry.NewSpan(r.Context(), "serve-porter-yaml-from-revision")
- defer span.End()
- r = r.Clone(ctx)
- project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
- cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
- appRevisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
- if reqErr != nil {
- err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
- return
- }
- request := &PorterYAMLFromRevisionRequest{}
- if ok := c.DecodeAndValidate(w, r, request); !ok {
- err := telemetry.Error(ctx, span, nil, "error decoding request")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
- return
- }
- getRevisionReq := connect.NewRequest(&porterv1.GetAppRevisionRequest{
- ProjectId: int64(project.ID),
- AppRevisionId: appRevisionID,
- })
- ccpResp, err := c.Config().ClusterControlPlaneClient.GetAppRevision(ctx, getRevisionReq)
- if err != nil {
- err = telemetry.Error(ctx, span, err, "error getting app revision")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- if ccpResp == nil || ccpResp.Msg == nil {
- err = telemetry.Error(ctx, span, nil, "get app revision response is nil")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- if ccpResp.Msg.AppRevision == nil {
- err = telemetry.Error(ctx, span, nil, "app revision is nil")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- appProto := ccpResp.Msg.AppRevision.App
- if appProto == nil {
- err = telemetry.Error(ctx, span, nil, "app proto is nil")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- agent, err := c.GetAgent(r, cluster, "")
- if err != nil {
- err = telemetry.Error(ctx, span, err, "error getting agent for cluster")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- appRevisionUUID, err := uuid.Parse(appRevisionID)
- if err != nil {
- err = telemetry.Error(ctx, span, err, "error parsing app revision id")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
- return
- }
- env, defaultEnvGroupName, err := defaultEnvGroup(ctx, formatDefaultEnvGroupInput{
- ProjectID: project.ID,
- Cluster: cluster,
- AppRevisionID: appRevisionUUID,
- appYAML: v2.PorterApp{},
- K8sAgent: agent,
- ClusterControlPlaneClient: c.Config().ClusterControlPlaneClient,
- PorterAppRepository: c.Repo().PorterApp(),
- })
- if err != nil {
- err = telemetry.Error(ctx, span, err, "error formatting default env group")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- app, err := v2.AppFromProto(appProto)
- if err != nil {
- err = telemetry.Error(ctx, span, err, "error converting app proto to porter yaml")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- var envGroups []string
- for _, envGroup := range app.EnvGroups {
- if !strings.Contains(envGroup, defaultEnvGroupName) {
- envGroups = append(envGroups, envGroup)
- }
- }
- app.Env = env
- app.EnvGroups = envGroups
- app = zeroOutValues(app)
- if request.ShouldFormatForExport {
- app = formatForExport(app, c.Config().ServerConf.AppRootDomain)
- }
- // sort services by name
- sortedServices := app.Services
- sort.Slice(sortedServices, func(i, j int) bool {
- serviceTypeSortPriorityA, ok := serviceTypeSortPriority[sortedServices[i].Type]
- if !ok {
- return false
- }
- serviceTypeSortPriorityB, ok := serviceTypeSortPriority[sortedServices[j].Type]
- if !ok {
- return false
- }
- if serviceTypeSortPriorityA != serviceTypeSortPriorityB {
- return serviceTypeSortPriorityA < serviceTypeSortPriorityB
- }
- return sortedServices[i].Name < sortedServices[j].Name
- })
- app.Services = sortedServices
- servicesWithDomainsSorted := app.Services
- for i := range servicesWithDomainsSorted {
- sortedDomains := servicesWithDomainsSorted[i].Domains
- sort.Slice(sortedDomains, func(i, j int) bool {
- return sortedDomains[i].Name < sortedDomains[j].Name
- })
- servicesWithDomainsSorted[i].Domains = sortedDomains
- }
- app.Services = servicesWithDomainsSorted
- // sort env variables by key
- sortedEnv := app.Env
- sort.Slice(sortedEnv, func(i, j int) bool {
- return sortedEnv[i].Key < sortedEnv[j].Key
- })
- app.Env = sortedEnv
- porterYAMLString, err := yaml.Marshal(app)
- if err != nil {
- err = telemetry.Error(ctx, span, err, "error marshaling porter yaml")
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
- return
- }
- b64String := base64.StdEncoding.EncodeToString(porterYAMLString)
- response := &PorterYAMLFromRevisionResponse{
- B64PorterYAML: b64String,
- }
- c.WriteResult(w, r, response)
- }
- type formatDefaultEnvGroupInput struct {
- ProjectID uint
- Cluster *models.Cluster
- AppRevisionID uuid.UUID
- appYAML v2.PorterApp
- K8sAgent *kubernetes.Agent
- ClusterControlPlaneClient porterv1connect.ClusterControlPlaneServiceClient
- PorterAppRepository repository.PorterAppRepository
- }
- func defaultEnvGroup(ctx context.Context, input formatDefaultEnvGroupInput) ([]v2.EnvVariableDefinition, string, error) {
- ctx, span := telemetry.NewSpan(ctx, "format-default-env-group")
- defer span.End()
- var env []v2.EnvVariableDefinition
- revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{
- AppRevisionID: input.AppRevisionID,
- ProjectID: input.ProjectID,
- CCPClient: input.ClusterControlPlaneClient,
- })
- if err != nil {
- return env, "", telemetry.Error(ctx, span, err, "error getting app revision")
- }
- decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
- if err != nil {
- return env, "", telemetry.Error(ctx, span, err, "error decoding base proto")
- }
- appProto := &porterv1.PorterApp{}
- err = helpers.UnmarshalContractObject(decoded, appProto)
- if err != nil {
- return env, "", telemetry.Error(ctx, span, err, "error unmarshalling app proto")
- }
- deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{
- ProjectID: int64(input.ProjectID),
- ClusterID: int64(input.Cluster.ID),
- DeploymentTargetID: revision.DeploymentTarget.ID,
- CCPClient: input.ClusterControlPlaneClient,
- })
- if err != nil {
- return env, "", telemetry.Error(ctx, span, err, "error getting deployment target details")
- }
- revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
- ProjectID: input.ProjectID,
- ClusterID: int(input.Cluster.ID),
- Revision: revision,
- DeploymentTarget: deploymentTarget,
- K8SAgent: input.K8sAgent,
- PorterAppRepository: input.PorterAppRepository,
- })
- if err != nil {
- return env, "", telemetry.Error(ctx, span, err, "error attaching env to revision")
- }
- for key, val := range revisionWithEnv.Env.Variables {
- env = append(env, v2.EnvVariableDefinition{
- Key: key,
- Source: v2.EnvVariableSource_Value,
- Value: v2.EnvValueOptional{
- Value: val,
- IsSet: true,
- },
- })
- }
- for key, val := range revisionWithEnv.Env.SecretVariables {
- env = append(env, v2.EnvVariableDefinition{
- Key: key,
- Source: v2.EnvVariableSource_Value,
- Value: v2.EnvValueOptional{
- Value: val,
- IsSet: true,
- },
- })
- }
- for _, ev := range appProto.Env {
- if ev.Source == porterv1.EnvVariableSource_ENV_VARIABLE_SOURCE_FROM_APP {
- fromAppProto := ev.GetFromApp()
- if fromAppProto == nil {
- continue
- }
- fromApp, err := v2.EnvVarFromAppFromProto(fromAppProto)
- if err != nil {
- return env, "", telemetry.Error(ctx, span, err, "error converting env var from app to proto")
- }
- envVar := v2.EnvVariableDefinition{
- Key: ev.Key,
- Source: v2.EnvVariableSource_FromApp,
- FromApp: fromApp,
- }
- env = append(env, envVar)
- }
- }
- return env, revisionWithEnv.Env.Name, nil
- }
- func formatForExport(app v2.PorterApp, appRootDomain string) v2.PorterApp {
- for i := range app.Services {
- app.Services[i] = filterNewServiceValues(app.Services[i])
- if app.Services[i].Type == v2.ServiceType_Web {
- // remove porter domains
- var filteredDomains []v2.Domains
- for _, domain := range app.Services[i].Domains {
- if !strings.HasSuffix(domain.Name, appRootDomain) {
- filteredDomains = append(filteredDomains, domain)
- }
- }
- app.Services[i].Domains = filteredDomains
- }
- }
- if app.Predeploy != nil {
- predeploy := filterNewServiceValues(*app.Predeploy)
- app.Predeploy = &predeploy
- }
- // don't show image or commit sha if build is present
- if app.Build != nil {
- app.Image = nil
- app.Build.CommitSHA = ""
- }
- // remove env secrets from env
- var filtered []v2.EnvVariableDefinition
- for _, ev := range app.Env {
- if ev.Value.Value != "********" {
- filtered = append(filtered, ev)
- }
- }
- app.Env = filtered
- // don't show env group versions
- for i := range app.EnvGroups {
- app.EnvGroups[i] = strings.Split(app.EnvGroups[i], ":")[0]
- }
- return app
- }
- // this "no-op" ensures that new fields are always zero-ed out in the exported yaml, until we specifically add it here
- func filterNewServiceValues(service v2.Service) v2.Service {
- return v2.Service{
- Name: service.Name,
- Run: service.Run,
- Type: service.Type,
- Instances: service.Instances,
- CpuCores: service.CpuCores,
- RamMegabytes: service.RamMegabytes,
- GpuCoresNvidia: service.GpuCoresNvidia,
- GPU: service.GPU,
- SmartOptimization: service.SmartOptimization,
- TerminationGracePeriodSeconds: service.TerminationGracePeriodSeconds,
- Port: service.Port,
- Autoscaling: service.Autoscaling,
- Domains: service.Domains,
- HealthCheck: service.HealthCheck,
- AllowConcurrent: service.AllowConcurrent,
- Cron: service.Cron,
- SuspendCron: service.SuspendCron,
- TimeoutSeconds: service.TimeoutSeconds,
- Private: service.Private,
- IngressAnnotations: service.IngressAnnotations,
- DisableTLS: service.DisableTLS,
- Sleep: service.Sleep,
- }
- }
- func zeroOutValues(app v2.PorterApp) v2.PorterApp {
- for i := range app.Services {
- // remove smart optimization
- app.Services[i].SmartOptimization = nil
- if app.Services[i].GPU != nil && !app.Services[i].GPU.Enabled {
- app.Services[i].GPU = nil
- }
- // remove launcher
- if app.Services[i].Run != nil {
- launcherLess := strings.TrimPrefix(*app.Services[i].Run, "launcher ")
- launcherLess = strings.TrimPrefix(launcherLess, "/cnb/lifecycle/launcher ")
- app.Services[i].Run = &launcherLess
- }
- switch app.Services[i].Type {
- case v2.ServiceType_Web:
- // remove autoscaling if not enabled
- if app.Services[i].Autoscaling != nil && !app.Services[i].Autoscaling.Enabled {
- app.Services[i].Autoscaling = nil
- }
- // remove health if not enabled
- if app.Services[i].HealthCheck != nil && !app.Services[i].HealthCheck.Enabled {
- app.Services[i].HealthCheck = nil
- }
- // don't show disableTLS if not enabled
- if app.Services[i].DisableTLS != nil && !*app.Services[i].DisableTLS {
- app.Services[i].DisableTLS = nil
- }
- // remove private if not enabled
- if app.Services[i].Private != nil && !*app.Services[i].Private {
- app.Services[i].Private = nil
- }
- case v2.ServiceType_Worker:
- // remove autoscaling if not enabled
- if app.Services[i].Autoscaling != nil && !app.Services[i].Autoscaling.Enabled {
- app.Services[i].Autoscaling = nil
- }
- // remove health if not enabled
- if app.Services[i].HealthCheck != nil && !app.Services[i].HealthCheck.Enabled {
- app.Services[i].HealthCheck = nil
- }
- // remove port
- app.Services[i].Port = 0
- case v2.ServiceType_Job:
- // remove port
- app.Services[i].Port = 0
- // remove instances
- app.Services[i].Instances = nil
- // remove suspendCron if not enabled
- if app.Services[i].SuspendCron != nil && !*app.Services[i].SuspendCron {
- app.Services[i].SuspendCron = nil
- }
- // remove allowConcurrency if not enabled
- if app.Services[i].AllowConcurrent != nil && !*app.Services[i].AllowConcurrent {
- app.Services[i].AllowConcurrent = nil
- }
- }
- }
- if app.Predeploy != nil {
- // remove name
- app.Predeploy.Name = ""
- // remove type
- app.Predeploy.Type = ""
- // remove smart optimization
- app.Predeploy.SmartOptimization = nil
- // remove launcher
- if app.Predeploy.Run != nil {
- launcherLess := strings.TrimPrefix(*app.Predeploy.Run, "launcher ")
- launcherLess = strings.TrimPrefix(launcherLess, "/cnb/lifecycle/launcher ")
- app.Predeploy.Run = &launcherLess
- }
- // remove port
- app.Predeploy.Port = 0
- // remove instances
- app.Predeploy.Instances = nil
- // remove suspendCron
- app.Predeploy.SuspendCron = nil
- // remove allowConcurrency
- app.Predeploy.AllowConcurrent = nil
- // remove timeout
- app.Predeploy.TimeoutSeconds = 0
- // remove gpu
- app.Predeploy.GPU = nil
- }
- if app.InitialDeploy != nil {
- // remove name
- app.InitialDeploy.Name = ""
- // remove type
- app.InitialDeploy.Type = ""
- // remove smart optimization
- app.InitialDeploy.SmartOptimization = nil
- // remove launcher
- if app.InitialDeploy.Run != nil {
- launcherLess := strings.TrimPrefix(*app.InitialDeploy.Run, "launcher ")
- launcherLess = strings.TrimPrefix(launcherLess, "/cnb/lifecycle/launcher ")
- app.InitialDeploy.Run = &launcherLess
- }
- // remove port
- app.InitialDeploy.Port = 0
- // remove instances
- app.InitialDeploy.Instances = nil
- // remove suspendCron
- app.InitialDeploy.SuspendCron = nil
- // remove allowConcurrency
- app.InitialDeploy.AllowConcurrent = nil
- // remove timeout
- app.InitialDeploy.TimeoutSeconds = 0
- // remove gpu
- app.InitialDeploy.GPU = nil
- }
- return app
- }
- var serviceTypeSortPriority = map[v2.ServiceType]int{
- v2.ServiceType_Web: 0,
- v2.ServiceType_Worker: 1,
- v2.ServiceType_Job: 2,
- }
|