add feature flag for resource causality
since the causality query is so expensive, it makes sense to give cluster admins the ability to disable the endpoints and page view. this is the first time we introduced the idea of feature flags to the frontend. my solution is to include the current state of the feature flags in the `/api/v1/info` endpoint, this can then be parsed by the frontend and stored in `session` making it available to any elm code that might require it. if resource causality is disabled (default), the `view all` buttons in the expended resource version will not show up and trying to navigate to the causality pages directly via url will result in a standard 404 not found page. another change is the stauts code for the causality endpoint will be 403 Forbidden if causality is disabled, and 422 UnprocessableEntity if the graph is too large. Signed-off-by: Bohan Chen <bochen@pivotal.io>
This commit is contained in:
parent
5b9c07e892
commit
7eb35f23bf
|
@ -15,6 +15,15 @@ func (s *Server) Info(w http.ResponseWriter, r *http.Request) {
|
|||
WorkerVersion: s.workerVersion,
|
||||
ExternalURL: s.externalURL,
|
||||
ClusterName: s.clusterName,
|
||||
FeatureFlags: map[string]bool{
|
||||
"global_resources": atc.EnableGlobalResources,
|
||||
"redact_secrets": atc.EnableRedactSecrets,
|
||||
"build_rerun": atc.EnableBuildRerunWhenWorkerDisappears,
|
||||
"across_step": atc.EnableAcrossStep,
|
||||
"pipeline_instances": atc.EnablePipelineInstances,
|
||||
"cache_streamed_volumes": atc.EnableCacheStreamedVolumes,
|
||||
"resource_causality": atc.EnableResourceCausality,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("failed-to-encode-info", err)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"code.cloudfoundry.org/lager"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/concourse/concourse/atc/db"
|
||||
)
|
||||
|
||||
|
@ -23,6 +24,12 @@ func (s *Server) getResourceCausality(direction db.CausalityDirection, pipeline
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := s.logger.Session(fmt.Sprintf("%v-causality", direction))
|
||||
|
||||
if !atc.EnableResourceCausality {
|
||||
logger.Info("causality-disabled")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
versionIDString := r.FormValue(":resource_config_version_id")
|
||||
resourceName := r.FormValue(":resource_name")
|
||||
versionID, _ := strconv.Atoi(versionIDString)
|
||||
|
@ -44,7 +51,7 @@ func (s *Server) getResourceCausality(direction db.CausalityDirection, pipeline
|
|||
if err != nil {
|
||||
if err == db.ErrTooManyBuilds || err == db.ErrTooManyResourceVersions {
|
||||
logger.Error("too-many-nodes", err, lager.Data{"resource-name": resourceName, "resource-config-version": versionID})
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
} else {
|
||||
logger.Error("failed-to-fetch", err, lager.Data{"resource-name": resourceName, "resource-config-version": versionID})
|
||||
|
|
|
@ -251,6 +251,7 @@ type RunCommand struct {
|
|||
EnablePipelineInstances bool `long:"enable-pipeline-instances" description:"Enable pipeline instances"`
|
||||
EnableP2PVolumeStreaming bool `long:"enable-p2p-volume-streaming" description:"Enable P2P volume streaming. NOTE: All workers must be on the same LAN network"`
|
||||
EnableCacheStreamedVolumes bool `long:"enable-cache-streamed-volumes" description:"When enabled, streamed resource volumes will be cached on the destination worker."`
|
||||
EnableResourceCausality bool `long:"enable-resource-causality" description:"Enable the resource causality page. Computing causality can be expensive for the database. "`
|
||||
} `group:"Feature Flags"`
|
||||
|
||||
BaseResourceTypeDefaults flag.File `long:"base-resource-type-defaults" description:"Base resource type defaults"`
|
||||
|
@ -522,6 +523,7 @@ func (cmd *RunCommand) Runner(positionalArguments []string) (ifrit.Runner, error
|
|||
atc.EnableAcrossStep = cmd.FeatureFlags.EnableAcrossStep
|
||||
atc.EnablePipelineInstances = cmd.FeatureFlags.EnablePipelineInstances
|
||||
atc.EnableCacheStreamedVolumes = cmd.FeatureFlags.EnableCacheStreamedVolumes
|
||||
atc.EnableResourceCausality = cmd.FeatureFlags.EnableResourceCausality
|
||||
|
||||
if cmd.BaseResourceTypeDefaults.Path() != "" {
|
||||
content, err := ioutil.ReadFile(cmd.BaseResourceTypeDefaults.Path())
|
||||
|
|
|
@ -7,4 +7,5 @@ var (
|
|||
EnableAcrossStep bool
|
||||
EnablePipelineInstances bool
|
||||
EnableCacheStreamedVolumes bool
|
||||
EnableResourceCausality bool
|
||||
)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package atc
|
||||
|
||||
type Info struct {
|
||||
Version string `json:"version"`
|
||||
WorkerVersion string `json:"worker_version"`
|
||||
ExternalURL string `json:"external_url,omitempty"`
|
||||
ClusterName string `json:"cluster_name,omitempty"`
|
||||
Version string `json:"version"`
|
||||
WorkerVersion string `json:"worker_version"`
|
||||
FeatureFlags map[string]bool `json:"feature_flags"`
|
||||
ExternalURL string `json:"external_url,omitempty"`
|
||||
ClusterName string `json:"cluster_name,omitempty"`
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ services:
|
|||
CONCOURSE_ENABLE_PIPELINE_INSTANCES: "true"
|
||||
CONCOURSE_ENABLE_ACROSS_STEP: "true"
|
||||
CONCOURSE_ENABLE_CACHE_STREAMED_VOLUMES: "true"
|
||||
CONCOURSE_ENABLE_RESOURCE_CAUSALITY: "true"
|
||||
|
||||
worker:
|
||||
build: .
|
||||
|
|
|
@ -73,6 +73,9 @@ init flags url =
|
|||
, hovered = HoverState.NoHover
|
||||
, clusterName = ""
|
||||
, version = ""
|
||||
, featureFlags =
|
||||
{ resourceCausality = False
|
||||
}
|
||||
, turbulenceImgSrc = flags.turbulenceImgSrc
|
||||
, notFoundImgSrc = flags.notFoundImgSrc
|
||||
, csrfToken = flags.csrfToken
|
||||
|
@ -205,13 +208,13 @@ handleCallback callback model =
|
|||
in
|
||||
subpageHandleCallback callback ( { model | session = newSession }, [] )
|
||||
|
||||
ClusterInfoFetched (Ok { clusterName, version }) ->
|
||||
ClusterInfoFetched (Ok { clusterName, version, featureFlags }) ->
|
||||
let
|
||||
session =
|
||||
model.session
|
||||
|
||||
newSession =
|
||||
{ session | clusterName = clusterName, version = version }
|
||||
{ session | clusterName = clusterName, version = version, featureFlags = featureFlags }
|
||||
in
|
||||
subpageHandleCallback callback ( { model | session = newSession }, [] )
|
||||
|
||||
|
@ -263,7 +266,7 @@ subpageHandleCallback callback ( model, effects ) =
|
|||
( subModel, newEffects ) =
|
||||
( model.subModel, effects )
|
||||
|> SubPage.handleCallback callback model.session
|
||||
|> SubPage.handleNotFound model.session.notFoundImgSrc model.session.route
|
||||
|> SubPage.handleNotFound model.session
|
||||
in
|
||||
( { model | subModel = subModel }, newEffects )
|
||||
|
||||
|
@ -301,7 +304,7 @@ update msg model =
|
|||
( subModel, subEffects ) =
|
||||
( model.subModel, [] )
|
||||
|> SubPage.update model.session m
|
||||
|> SubPage.handleNotFound model.session.notFoundImgSrc model.session.route
|
||||
|> SubPage.handleNotFound model.session
|
||||
|
||||
( session, sessionEffects ) =
|
||||
SideBar.update m model.session
|
||||
|
@ -323,7 +326,7 @@ handleDelivery delivery model =
|
|||
( newSubmodel, subPageEffects ) =
|
||||
( model.subModel, [] )
|
||||
|> SubPage.handleDelivery model.session delivery
|
||||
|> SubPage.handleNotFound model.session.notFoundImgSrc model.session.route
|
||||
|> SubPage.handleNotFound model.session
|
||||
|
||||
( newModel, applicationEffects ) =
|
||||
handleDeliveryForApplication
|
||||
|
|
|
@ -11,6 +11,7 @@ type alias Session =
|
|||
{ userState : UserState
|
||||
, clusterName : String
|
||||
, version : String
|
||||
, featureFlags : Concourse.FeatureFlags
|
||||
, turbulenceImgSrc : String
|
||||
, notFoundImgSrc : String
|
||||
, csrfToken : Concourse.CSRFToken
|
||||
|
|
|
@ -148,9 +148,12 @@ handleCallback callback ( model, effects ) =
|
|||
if status.code == 401 then
|
||||
( model, effects ++ [ RedirectToLogin ] )
|
||||
|
||||
else if status.code == 403 then
|
||||
else if status.code == 422 then
|
||||
( { model | pageStatus = Err TooManyNodes }, effects )
|
||||
|
||||
else if status.code == 403 then
|
||||
( { model | pageStatus = Err NotFound }, effects )
|
||||
|
||||
else if status.code == 404 then
|
||||
( { model | pageStatus = Err NotFound }, effects )
|
||||
|
||||
|
@ -213,17 +216,21 @@ update msg ( model, effects ) =
|
|||
( model, effects )
|
||||
|
||||
|
||||
getUpdateMessage : Model -> UpdateMsg
|
||||
getUpdateMessage model =
|
||||
case model.pageStatus of
|
||||
Err NotFound ->
|
||||
UpdateMsg.NotFound
|
||||
getUpdateMessage : Session -> Model -> UpdateMsg
|
||||
getUpdateMessage session model =
|
||||
if not session.featureFlags.resourceCausality then
|
||||
UpdateMsg.NotFound
|
||||
|
||||
Err _ ->
|
||||
UpdateMsg.AOK
|
||||
else
|
||||
case model.pageStatus of
|
||||
Err NotFound ->
|
||||
UpdateMsg.NotFound
|
||||
|
||||
Ok () ->
|
||||
UpdateMsg.AOK
|
||||
Err _ ->
|
||||
UpdateMsg.AOK
|
||||
|
||||
Ok () ->
|
||||
UpdateMsg.AOK
|
||||
|
||||
|
||||
view : Session -> Model -> Html Message
|
||||
|
|
|
@ -22,6 +22,7 @@ module Concourse exposing
|
|||
, CausalityResourceVersion
|
||||
, ClusterInfo
|
||||
, DatabaseID
|
||||
, FeatureFlags
|
||||
, HookedPlan
|
||||
, InstanceGroupIdentifier
|
||||
, InstanceVars
|
||||
|
@ -181,9 +182,11 @@ type alias BuildId =
|
|||
type alias BuildName =
|
||||
String
|
||||
|
||||
|
||||
type alias BuildCreatedBy =
|
||||
Maybe String
|
||||
|
||||
|
||||
type alias JobBuildIdentifier =
|
||||
{ teamName : TeamName
|
||||
, pipelineName : PipelineName
|
||||
|
@ -202,7 +205,7 @@ type alias Build =
|
|||
, duration : BuildDuration
|
||||
, comment : String
|
||||
, reapTime : Maybe Time.Posix
|
||||
, createdBy: BuildCreatedBy
|
||||
, createdBy : BuildCreatedBy
|
||||
}
|
||||
|
||||
|
||||
|
@ -834,9 +837,21 @@ decodeBuildStepAcross =
|
|||
-- Info
|
||||
|
||||
|
||||
type alias FeatureFlags =
|
||||
{ resourceCausality : Bool
|
||||
}
|
||||
|
||||
|
||||
decodeFeatureFlags : Json.Decode.Decoder FeatureFlags
|
||||
decodeFeatureFlags =
|
||||
Json.Decode.succeed FeatureFlags
|
||||
|> andMap (Json.Decode.field "resource_causality" Json.Decode.bool)
|
||||
|
||||
|
||||
type alias ClusterInfo =
|
||||
{ version : String
|
||||
, clusterName : String
|
||||
, featureFlags : FeatureFlags
|
||||
}
|
||||
|
||||
|
||||
|
@ -845,6 +860,7 @@ decodeInfo =
|
|||
Json.Decode.succeed ClusterInfo
|
||||
|> andMap (Json.Decode.field "version" Json.Decode.string)
|
||||
|> andMap (defaultTo "" <| Json.Decode.field "cluster_name" Json.Decode.string)
|
||||
|> andMap (Json.Decode.field "feature_flags" decodeFeatureFlags)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1226,6 +1226,7 @@ body :
|
|||
, pipelines : WebData (List Concourse.Pipeline)
|
||||
, hovered : HoverState.HoverState
|
||||
, timeZone : Time.Zone
|
||||
, featureFlags : Concourse.FeatureFlags
|
||||
}
|
||||
-> Model
|
||||
-> Html Message
|
||||
|
@ -1745,7 +1746,10 @@ isPipelineArchived session id =
|
|||
|
||||
|
||||
viewVersionedResources :
|
||||
{ a | pipelines : WebData (List Concourse.Pipeline) }
|
||||
{ a
|
||||
| pipelines : WebData (List Concourse.Pipeline)
|
||||
, featureFlags : Concourse.FeatureFlags
|
||||
}
|
||||
->
|
||||
{ b
|
||||
| versions : Paginated Models.Version
|
||||
|
@ -1760,16 +1764,17 @@ viewVersionedResources session model =
|
|||
in
|
||||
model
|
||||
|> versions
|
||||
|> List.map (\v -> viewVersionedResource { version = v, archived = archived })
|
||||
|> List.map (\v -> viewVersionedResource { version = v, archived = archived, causalityEnabled = session.featureFlags.resourceCausality })
|
||||
|> Html.ul [ class "list list-collapsable list-enableDisable resource-versions" ]
|
||||
|
||||
|
||||
viewVersionedResource :
|
||||
{ version : VersionPresenter
|
||||
, archived : Bool
|
||||
, causalityEnabled : Bool
|
||||
}
|
||||
-> Html Message
|
||||
viewVersionedResource { version, archived } =
|
||||
viewVersionedResource { version, archived, causalityEnabled } =
|
||||
Html.li
|
||||
(case ( version.pinState, version.enabled ) of
|
||||
( Disabled, _ ) ->
|
||||
|
@ -1816,6 +1821,7 @@ viewVersionedResource { version, archived } =
|
|||
, outputOf = version.outputOf
|
||||
, versionId = version.id
|
||||
, metadata = version.metadata
|
||||
, causalityEnabled = causalityEnabled
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -1831,15 +1837,16 @@ viewVersionBody :
|
|||
, outputOf : List Concourse.Build
|
||||
, versionId : Concourse.VersionedResourceIdentifier
|
||||
, metadata : Concourse.Metadata
|
||||
, causalityEnabled : Bool
|
||||
}
|
||||
-> Html Message
|
||||
viewVersionBody { inputTo, outputOf, versionId, metadata } =
|
||||
viewVersionBody { inputTo, outputOf, versionId, metadata, causalityEnabled } =
|
||||
Html.div
|
||||
[ style "display" "flex"
|
||||
, style "padding" "5px 10px"
|
||||
]
|
||||
[ viewInputsOrOutputs Concourse.Downstream versionId inputTo
|
||||
, viewInputsOrOutputs Concourse.Upstream versionId outputOf
|
||||
[ viewInputsOrOutputs causalityEnabled Concourse.Downstream versionId inputTo
|
||||
, viewInputsOrOutputs causalityEnabled Concourse.Upstream versionId outputOf
|
||||
, Html.div [ class "vri metadata-container" ]
|
||||
[ Html.div [ class "list-collapsable-title" ] [ Html.text "metadata" ]
|
||||
, viewMetadata metadata
|
||||
|
@ -1967,20 +1974,20 @@ viewVersion attrs version =
|
|||
|> DictView.view attrs
|
||||
|
||||
|
||||
viewInputsOrOutputs : Concourse.CausalityDirection -> Concourse.VersionedResourceIdentifier -> List Concourse.Build -> Html Message
|
||||
viewInputsOrOutputs direction versionId builds =
|
||||
viewInputsOrOutputs : Bool -> Concourse.CausalityDirection -> Concourse.VersionedResourceIdentifier -> List Concourse.Build -> Html Message
|
||||
viewInputsOrOutputs causalityEnabled direction versionId builds =
|
||||
Html.div [ class "vri" ] <|
|
||||
List.concat
|
||||
[ [ Html.div
|
||||
[ style "line-height" "25px" ]
|
||||
[ viewCausalityButton direction versionId ]
|
||||
[ viewCausalityButton causalityEnabled direction versionId ]
|
||||
]
|
||||
, viewBuilds <| listToMap builds
|
||||
]
|
||||
|
||||
|
||||
viewCausalityButton : Concourse.CausalityDirection -> Concourse.VersionedResourceIdentifier -> Html Message
|
||||
viewCausalityButton dir versionId =
|
||||
viewCausalityButton : Bool -> Concourse.CausalityDirection -> Concourse.VersionedResourceIdentifier -> Html Message
|
||||
viewCausalityButton enabled dir versionId =
|
||||
let
|
||||
link =
|
||||
Routes.Causality
|
||||
|
@ -2003,21 +2010,25 @@ viewCausalityButton dir versionId =
|
|||
, onClick <| GoToRoute link
|
||||
]
|
||||
in
|
||||
Html.div
|
||||
[ style "line-height" "25px"
|
||||
, style "display" "flex"
|
||||
, style "justify-content" "space-between"
|
||||
]
|
||||
[ Html.text text
|
||||
, Html.a
|
||||
([ href (Routes.toString link)
|
||||
, id (toHtmlID <| domID)
|
||||
]
|
||||
++ eventHandlers
|
||||
++ Resource.Styles.causalityButton
|
||||
)
|
||||
[ Html.text "view all" ]
|
||||
]
|
||||
if enabled then
|
||||
Html.div
|
||||
[ style "line-height" "25px"
|
||||
, style "display" "flex"
|
||||
, style "justify-content" "space-between"
|
||||
]
|
||||
[ Html.text text
|
||||
, Html.a
|
||||
([ href (Routes.toString link)
|
||||
, id (toHtmlID <| domID)
|
||||
]
|
||||
++ eventHandlers
|
||||
++ Resource.Styles.causalityButton
|
||||
)
|
||||
[ Html.text "view all" ]
|
||||
]
|
||||
|
||||
else
|
||||
Html.text text
|
||||
|
||||
|
||||
viewMetadata : Concourse.Metadata -> Html Message
|
||||
|
|
|
@ -115,13 +115,13 @@ init session route =
|
|||
|> Tuple.mapFirst CausalityModel
|
||||
|
||||
|
||||
handleNotFound : String -> Routes.Route -> ET Model
|
||||
handleNotFound notFound route ( model, effects ) =
|
||||
case getUpdateMessage model of
|
||||
handleNotFound : Session -> ET Model
|
||||
handleNotFound session ( model, effects ) =
|
||||
case getUpdateMessage session model of
|
||||
UpdateMsg.NotFound ->
|
||||
let
|
||||
( newModel, newEffects ) =
|
||||
NotFound.init { notFoundImgSrc = notFound, route = route }
|
||||
NotFound.init { notFoundImgSrc = session.notFoundImgSrc, route = session.route }
|
||||
in
|
||||
( NotFoundModel newModel, effects ++ newEffects )
|
||||
|
||||
|
@ -129,8 +129,8 @@ handleNotFound notFound route ( model, effects ) =
|
|||
( model, effects )
|
||||
|
||||
|
||||
getUpdateMessage : Model -> UpdateMsg
|
||||
getUpdateMessage model =
|
||||
getUpdateMessage : Session -> Model -> UpdateMsg
|
||||
getUpdateMessage session model =
|
||||
case model of
|
||||
BuildModel mdl ->
|
||||
Build.getUpdateMessage mdl
|
||||
|
@ -145,7 +145,7 @@ getUpdateMessage model =
|
|||
Pipeline.getUpdateMessage mdl
|
||||
|
||||
CausalityModel mdl ->
|
||||
Causality.getUpdateMessage mdl
|
||||
Causality.getUpdateMessage session mdl
|
||||
|
||||
_ ->
|
||||
UpdateMsg.AOK
|
||||
|
|
Loading…
Reference in New Issue