Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support always proxying requests for Docker rules #30

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions manifest/Scarf/Manifest.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Data.Aeson.Encode.Pretty (encodePretty)
import Data.Bifunctor (second)
import Data.ByteString.Lazy (ByteString)
import Data.HashMap.Strict qualified as HashMap
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as Text
import Scarf.Gateway.ImagePattern qualified as ImagePattern
Expand Down Expand Up @@ -74,7 +75,8 @@ data ManifestRule
-- | e.g. @"library/hello-world"@
manifestRuleRepositoryName :: !Text,
-- | e.g. @docker.io@, @ghcr.io@
manifestRuleBackendRegistry :: !Text
manifestRuleBackendRegistry :: !Text,
manifestRuleAlwaysProxy :: !(Maybe Bool)
}
| ManifestDockerRuleV2
{ -- | Owner of the package
Expand All @@ -86,7 +88,8 @@ data ManifestRule
-- | Id of the corresponding rule in the database
manifestRuleId :: !Text,
-- | e.g. @docker.io@
manifestRuleBackendRegistry :: !Text
manifestRuleBackendRegistry :: !Text,
manifestRuleAlwaysProxy :: !(Maybe Bool)
}
| ManifestFileRuleV1
{ -- | Package name
Expand Down Expand Up @@ -204,13 +207,15 @@ instance FromJSON ManifestRule where
<*> o .: "domain"
<*> o .: "repository-name"
<*> o .: "registry"
<*> o .:? "always-proxy"
"docker-v2" ->
ManifestDockerRuleV2
<$> o .:? "owner" .!= ""
<*> o .: "domain"
<*> o .: "pattern"
<*> o .: "rule-id"
<*> o .: "registry"
<*> o .:? "always-proxy"
"file-v1" ->
ManifestFileRuleV1
<$> o .:? "package-name" .!= ""
Expand Down Expand Up @@ -260,7 +265,8 @@ instance ToJSON ManifestRule where
"package-id" .= manifestRulePackageId,
"domain" .= manifestRuleDomain,
"repository-name" .= manifestRuleRepositoryName,
"registry" .= manifestRuleBackendRegistry
"registry" .= manifestRuleBackendRegistry,
"always-proxy" .= manifestRuleAlwaysProxy
]
toJSON ManifestDockerRuleV2 {..} =
object $
Expand All @@ -270,7 +276,8 @@ instance ToJSON ManifestRule where
"domain" .= manifestRuleDomain,
"registry" .= manifestRuleBackendRegistry,
"pattern" .= manifestRulePattern,
"rule-id" .= manifestRuleId
"rule-id" .= manifestRuleId,
"always-proxy" .= manifestRuleAlwaysProxy
]
toJSON ManifestFileRuleV1 {..} =
object $
Expand Down Expand Up @@ -357,11 +364,13 @@ manifestRuleToRule manifestRule = case manifestRule of
manifestRulePackageId
(Text.splitOn "/" manifestRuleRepositoryName)
manifestRuleBackendRegistry
(fromMaybe False manifestRuleAlwaysProxy)
ManifestDockerRuleV2 {..} ->
newDockerRuleV2
manifestRuleId
(ImagePattern.toText manifestRulePattern)
manifestRuleBackendRegistry
(fromMaybe False manifestRuleAlwaysProxy)
ManifestFileRuleV1 {..} ->
newFlatfileRule
manifestRulePackageId
Expand Down
32 changes: 26 additions & 6 deletions src/Scarf/Gateway/Rule.hs
Original file line number Diff line number Diff line change
Expand Up @@ -288,24 +288,44 @@ sanitizeDockerhubDomain domain = case domain of
"registry.hub.docker.com" -> "registry-1.docker.io"
domain -> domain

newDockerRuleV1 :: Text -> [Text] -> Text -> Rule
newDockerRuleV1 package image backendDomain =
newDockerRuleV1 ::
-- Package identifier
Text ->
-- | Docker image components. e.g. ["library", "hello-world"].
[Text] ->
-- | Docker registry requests are redirected or proxied to.
Text ->
-- | Flag indicating whether to always proxy and never redirect.
Bool ->
Rule
newDockerRuleV1 package image backendDomain alwaysProxy =
RuleDockerV1
DockerRuleV1
{ ruleImages = HashMap.singleton image package,
ruleBackendRegistry = Text.encodeUtf8 (sanitizeDockerhubDomain backendDomain)
ruleBackendRegistry = Text.encodeUtf8 (sanitizeDockerhubDomain backendDomain),
ruleAlwaysProxy = alwaysProxy
}

newDockerRuleV2 :: Text -> Text -> Text -> Rule
newDockerRuleV2 ruleId textPattern backendDomain =
newDockerRuleV2 ::
-- | Collection identifier
Text ->
-- | Pattern matching images to redirect or proxy
Text ->
-- | Docker registry requests are redirected or proxied to.
Text ->
-- | Flag indicating whether to always proxy and never redirect.
Bool ->
Rule
newDockerRuleV2 ruleId textPattern backendDomain alwaysProxy =
RuleDockerV2
DockerRuleV2
{ ruleImagePattern =
case ImagePattern.fromText textPattern of
Nothing -> error "Invalid Pattern"
Just p -> p,
ruleRuleId = ruleId,
ruleBackendRegistry = Text.encodeUtf8 (sanitizeDockerhubDomain backendDomain)
ruleBackendRegistry = Text.encodeUtf8 (sanitizeDockerhubDomain backendDomain),
ruleAlwaysProxy = alwaysProxy
}

-- | Unsafely parses the templates and returns a flatfile rule.
Expand Down
29 changes: 21 additions & 8 deletions src/Scarf/Gateway/Rule/Docker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ data DockerRuleV1 = DockerRuleV1
-- respective package ids.
ruleImages :: !(HashMap [Text] Text),
-- | Domain of the registry to redirect (or proxy) to.
ruleBackendRegistry :: !ByteString
ruleBackendRegistry :: !ByteString,
-- | Flag indicating whether to always proxy and never redirect.
ruleAlwaysProxy :: !Bool
}
deriving (Eq, Show)

Expand All @@ -41,7 +43,9 @@ data DockerRuleV2 = DockerRuleV2
-- | Id of the rule in the backend.
ruleRuleId :: !Text,
-- | Domain of the registry to redirect (or proxy) to.
ruleBackendRegistry :: !ByteString
ruleBackendRegistry :: !ByteString,
-- | Flag indicating whether to always proxy and never redirect.
ruleAlwaysProxy :: !Bool
}
deriving (Eq, Show)

Expand All @@ -62,24 +66,26 @@ shouldAlwaysProxy domain =
type DockerImageMatcher = [Text] -> Maybe (Either Text Text)

matchDockerRuleV1 :: (MonadMatch m) => DockerRuleV1 -> Request -> ResponseBuilder response -> m (Maybe response)
matchDockerRuleV1 DockerRuleV1 {ruleImages, ruleBackendRegistry} =
matchDockerRuleV1 DockerRuleV1 {ruleImages, ruleBackendRegistry, ruleAlwaysProxy} =
matchDocker
( \image ->
case HashMap.lookup image ruleImages of
Just packageId -> Just (Left packageId)
Nothing -> Nothing
)
ruleBackendRegistry
ruleAlwaysProxy

matchDockerRuleV2 :: (MonadMatch m) => DockerRuleV2 -> Request -> ResponseBuilder response -> m (Maybe response)
matchDockerRuleV2 DockerRuleV2 {ruleImagePattern, ruleRuleId, ruleBackendRegistry} =
matchDockerRuleV2 DockerRuleV2 {ruleImagePattern, ruleRuleId, ruleBackendRegistry, ruleAlwaysProxy} =
matchDocker
( \image ->
if ImagePattern.match ruleImagePattern image
then Just (Right ruleRuleId)
else Nothing
)
ruleBackendRegistry
ruleAlwaysProxy

type MonadMatch m = m ~ IO

Expand All @@ -95,14 +101,16 @@ matchDocker ::
DockerImageMatcher ->
-- | Backend registry
ByteString ->
-- | Flag indicating whether to always proxy and never redirect.
Bool ->
Request ->
ResponseBuilder response ->
m (Maybe response)
matchDocker matchImage backendRegistry Request {requestWai = request} responseBuilder@ResponseBuilder {..}
matchDocker matchImage backendRegistry alwaysProxy Request {requestWai = request} responseBuilder@ResponseBuilder {..}
| "/v2/_catalog" <- Wai.rawPathInfo request =
pure $ Just (notFound emptyCapture)
| "/v2/" <- Wai.rawPathInfo request =
pure $ Just $ redirectOrProxy request backendRegistry emptyCapture responseBuilder
pure $ Just $ redirectOrProxy request backendRegistry alwaysProxy emptyCapture responseBuilder
| ("v2" : image, _manifestsOrBlobsOrTags : reference : _) <-
break
(\x -> x == "manifests" || x == "blobs" || x == "tags")
Expand All @@ -116,7 +124,7 @@ matchDocker matchImage backendRegistry Request {requestWai = request} responseBu
dockerCapturePackage = either Just (const Nothing) result,
dockerCaptureAutoCreate = either (const Nothing) Just result
}
in pure $ Just $ redirectOrProxy request backendRegistry capture responseBuilder
in pure $ Just $ redirectOrProxy request backendRegistry alwaysProxy capture responseBuilder
| otherwise =
pure Nothing
where
Expand All @@ -134,10 +142,15 @@ matchDocker matchImage backendRegistry Request {requestWai = request} responseBu
redirectOrProxy ::
Wai.Request ->
ByteString ->
-- | Flag indicating whether to always proxy and never redirect.
Bool ->
RuleCapture ->
ResponseBuilder response ->
response
redirectOrProxy request domain !capture ResponseBuilder {..}
redirectOrProxy request domain alwaysProxy !capture ResponseBuilder {..}
| alwaysProxy =
-- Proxy request unconditionally
proxyTo (const capture) domain
| shouldRedirectDockerRequest request =
-- As with the Host header we have to respect the proxy protocol
-- when redirecting.
Expand Down
6 changes: 3 additions & 3 deletions test/Scarf/Gateway/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ test_gateway_docker_rule_v1 =
"Gateway, Docker Pull"
( newGatewayConfig
[ ( "test.scarf.sh",
[ newDockerRuleV1 "package-1" ["library", "hello-world"] "docker.io"
[ newDockerRuleV1 "package-1" ["library", "hello-world"] "docker.io" False
]
)
]
Expand Down Expand Up @@ -292,7 +292,7 @@ test_gateway_docker_rule_v1_proxied =
"Gateway, Docker Pull with proxied requests"
( newGatewayConfig
[ ( "test.scarf.sh",
[ newDockerRuleV1 "package-1" ["library", "hello-world"] "docker.io"
[ newDockerRuleV1 "package-1" ["library", "hello-world"] "docker.io" False
]
)
]
Expand Down Expand Up @@ -354,7 +354,7 @@ test_gateway_docker_rule_v2 =
"Gateway, Docker Pull (with auto package creation)"
( newGatewayConfig
[ ( "test.scarf.sh",
[ newDockerRuleV2 "test-rule-1" "library/*" "docker.io"
[ newDockerRuleV2 "test-rule-1" "library/*" "docker.io" False
]
)
]
Expand Down
Empty file.
14 changes: 14 additions & 0 deletions test/golden/docker-always-proxy-2.output.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
capture: |-
Just
DockerCapture
{ dockerCaptureImage = [ "library" , "proxy" ]
, dockerCaptureReference = "latest"
, dockerCaptureBackendRegistry = "ghcr.io"
, dockerCapturePackage =
Just "8717953c-3452-4ef7-9a14-b64dc19163b4"
, dockerCaptureAutoCreate = Nothing
}
headers:
Location: https://ghcr.io/v2/library/proxy/manifests/latest
X-This-Request-Was-Proxied: '1'
status: 307
13 changes: 13 additions & 0 deletions test/golden/docker-always-proxy-2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# A manifest download with a user-agent that is usually redirected
path: /v2/library/proxy/manifests/latest
headers:
User-Agent: Docker-Client
Host: cr.test.io
manifest:
rules:
- type: docker-v1
repository-name: library/proxy
domain: cr.test.io
registry: ghcr.io
package-id: "8717953c-3452-4ef7-9a14-b64dc19163b4"
always-proxy: true
Empty file.
14 changes: 14 additions & 0 deletions test/golden/docker-always-proxy.output.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
capture: |-
Just
DockerCapture
{ dockerCaptureImage = [ "library" , "proxy" ]
, dockerCaptureReference = "latest"
, dockerCaptureBackendRegistry = "ghcr.io"
, dockerCapturePackage =
Just "8717953c-3452-4ef7-9a14-b64dc19163b4"
, dockerCaptureAutoCreate = Nothing
}
headers:
Location: https://ghcr.io/v2/library/proxy/manifests/latest
X-This-Request-Was-Proxied: '1'
status: 307
11 changes: 11 additions & 0 deletions test/golden/docker-always-proxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
path: /v2/library/proxy/manifests/latest
headers:
Host: cr.test.io
manifest:
rules:
- type: docker-v1
repository-name: library/proxy
domain: cr.test.io
registry: ghcr.io
package-id: "8717953c-3452-4ef7-9a14-b64dc19163b4"
always-proxy: true
Loading