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

feat(gateway): _redirects file support #8890

Merged
merged 54 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5ae4e4e
WIP _redirects support
Apr 7, 2022
937e998
Handle forced redirects
Apr 20, 2022
7145efc
Return 404 where we did previously
Apr 20, 2022
7f13047
Remove go.mod replace
Apr 20, 2022
748a6ea
Remove log statements based on CodeQL results
Apr 20, 2022
ab87d10
Add missing test_kill_ipfs_daemon to sharness
Apr 20, 2022
acd5d1a
Deps changes
Apr 21, 2022
8958efe
Comment cleanup
Apr 27, 2022
f79b4b4
Any path resolution errors mean the file doesn't exist
Apr 27, 2022
99720d2
WIP comments
Apr 28, 2022
fc617bf
Check for root path CID before joining with _redirects
Apr 28, 2022
1f86713
DNSLink test
Apr 29, 2022
5d59ce9
Comments
Apr 29, 2022
3cfb84e
Fix placeholder and splat usage for 200 and 404
May 25, 2022
56db72e
go mod tidy
May 25, 2022
0e2a3a8
Move test case to car file
Jun 15, 2022
2e4357b
Update CAR file and test after updating CAR file for spec
Jun 15, 2022
6fcca56
go mod tidy
Jun 16, 2022
08ddc07
Use type for context.WithValue, per docs
Jun 16, 2022
babb6ad
More types
Jun 16, 2022
6830b1b
Remove forced redirect support, to avoid the performance hit
Jun 16, 2022
4d9bddf
Use justincjohnson/go-ipfs-redirects, which I'll switch to the ipfs o…
Jul 7, 2022
d2e6106
Switch to github.com/ipfs-shipyard/go-ipfs-redirects
Aug 11, 2022
3a614ef
Address feedback, correct car fixture
Aug 11, 2022
f66d64b
More feedback
Aug 11, 2022
bb94374
More feedback
Aug 11, 2022
2acbf70
go mod tidy
Aug 11, 2022
f4cf5a6
Fix test
Aug 11, 2022
46faca8
Error early if invalid status
Aug 11, 2022
f9cd964
Confirm CRLF line terminator
Aug 12, 2022
5153b9a
Add tests for invalid _redirects file
Aug 12, 2022
0570b1d
Add test with attempted forced redirect
Aug 12, 2022
f4b9ec2
Simplify getRootPath by using ipath.Path
Aug 12, 2022
3cd0c6d
Consolidate unixfs and non-unixfs handling into a single method.
Aug 12, 2022
4c8ccbd
Remove direct dependency on ucarion/urlpath
Aug 12, 2022
4282785
go mod tidy
Aug 12, 2022
b299f81
Cleanup after rebase
Aug 19, 2022
cf2ae8c
Revert unnecessary downgrade of a dep
Aug 19, 2022
2495b8d
go mod tidy
Sep 15, 2022
825fc1e
function rename, per lidel
Sep 15, 2022
24d248c
Switch from github.com/ipfs-shipyard/go-ipfs-redirects to github.com/…
Sep 15, 2022
9778197
Move status code validation logic to github.com/ipfs/go-ipfs-redirect…
Sep 15, 2022
6334c5a
fix comment, in part to avoid inconsistent go fmt issue with 1.19
Sep 15, 2022
f25ed84
Update comment
Sep 15, 2022
5b6d7e2
Fix test to match new output with status code parsing happening in go…
Sep 16, 2022
38ac69d
Add test for max file size
Sep 16, 2022
2a7b0b6
Refactor as requested in review
Sep 19, 2022
462cd1a
Missing changes from lidel
Sep 21, 2022
8bfa333
fix go mod tidy error in ci
Sep 21, 2022
eb7138d
chore: go-ipfs-redirects-file v0.1.0
lidel Sep 22, 2022
0ae8545
refactor: move handlePathResolution
lidel Sep 22, 2022
a9a0658
Serve custom 410 and 451 pages as well
Sep 23, 2022
e18d6c7
core: go-ipfs-redirects-file v0.1.1
lidel Sep 23, 2022
907dc6d
refactor: move serveLegacy404IfPresent
lidel Sep 23, 2022
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
4 changes: 3 additions & 1 deletion core/coreapi/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func (e *ipnsEntry) Value() path.Path {
return e.value
}

type requestContextKey string

// Publish announces new IPNS name and returns the new IPNS entry.
func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.NamePublishOption) (coreiface.IpnsEntry, error) {
ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Publish", trace.WithAttributes(attribute.String("path", p.String())))
Expand Down Expand Up @@ -76,7 +78,7 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam

if options.TTL != nil {
// nolint: staticcheck // non-backward compatible change
ctx = context.WithValue(ctx, "ipns-publish-ttl", *options.TTL)
ctx = context.WithValue(ctx, requestContextKey("ipns-publish-ttl"), *options.TTL)
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
}

eol := time.Now().Add(options.ValidTime)
Expand Down
28 changes: 9 additions & 19 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
mfs "github.com/ipfs/go-mfs"
path "github.com/ipfs/go-path"
"github.com/ipfs/go-path/resolver"
coreiface "github.com/ipfs/interface-go-ipfs-core"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
routing "github.com/libp2p/go-libp2p/core/routing"
prometheus "github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -378,30 +377,18 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
return
}

// Resolve path to the final DAG node for the ETag
resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)
switch err {
case nil:
case coreiface.ErrOffline:
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
return
default:
// if Accept is text/html, see if ipfs-404.html is present
if i.servePretty404IfPresent(w, r, contentPath) {
logger.Debugw("serve pretty 404 if present")
return
}
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest)
return
}

// Detect when explicit Accept header or ?format parameter are present
responseFormat, formatParams, err := customResponseFormat(r)
if err != nil {
webError(w, "error while processing the Accept header", err, http.StatusBadRequest)
return
}
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat))

resolvedPath, contentPath, ok := i.handlePathResolution(w, r, responseFormat, contentPath, logger)
if !ok {
return
}
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResolvedPath", resolvedPath.String()))

// Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified
Expand Down Expand Up @@ -450,7 +437,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
}
}

func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
// Deprecated: legacy ipfs-404.html files are superseded by _redirects file
// This is provided only for backward-compatibility, until websites migrate
// to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290)
func (i *gatewayHandler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
if err != nil {
return false
Expand Down
260 changes: 260 additions & 0 deletions core/corehttp/gateway_handler_unixfs__redirects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package corehttp

import (
"fmt"
"io"
"net/http"
gopath "path"
"strconv"
"strings"

files "github.com/ipfs/go-ipfs-files"
redirects "github.com/ipfs/go-ipfs-redirects-file"
coreiface "github.com/ipfs/interface-go-ipfs-core"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
"go.uber.org/zap"
)

// Resolve the provided path.
func (i *gatewayHandler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (ipath.Resolved, ipath.Path, bool) {
// Attempt to resolve the provided path.
resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)

switch err {
case nil:
return resolvedPath, contentPath, true
case coreiface.ErrOffline:
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
return nil, nil, false
default:
if isUnixfsResponseFormat(responseFormat) {
// The path can't be resolved.
// If we have origin isolation, attempt to handle any redirect rules.
if hasOriginIsolation(r) {
resolvedPath, contentPath, ok, hadMatchingRule := i.serveRedirectsIfPresent(w, r, resolvedPath, contentPath, logger)
if hadMatchingRule {
return resolvedPath, contentPath, ok
}
}

// if Accept is text/html, see if ipfs-404.html is present
// This logic isn't documented and will likely be removed at some point.
// Any 404 logic in _redirects above will have already run by this time, so it's really an extra fall back
if i.serveLegacy404IfPresent(w, r, contentPath) {
logger.Debugw("serve pretty 404 if present")
return nil, nil, false
}

// Fallback
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest)
return nil, nil, false
} else {
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusNotFound)
return nil, nil, false
}
}
}

// Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved`
// corresponding to that path. For UnixFS, path resolution is more involved if a `_redirects` file exists, stored
// underneath the root CID of the path.
//
// Example 1:
// If a path exists, we always return the `path.Resolved` corresponding to that path, regardless of the existence of a `_redirects` file.
//
// Example 2:
// If a path does not exist, usually we should return a `nil` resolution path and an error indicating that the path
// doesn't exist. However, a `_redirects` file may exist and contain a redirect rule that redirects that path to a different path.
// We need to evaluate the rule and perform the redirect if present.
//
// Example 3:
// Another possibility is that the path corresponds to a rewrite rule (i.e. a rule with a status of 200).
// In this case, we don't perform a redirect, but do need to return a `path.Resolved` and `path.Path` corresponding to
// the rewrite destination path.
//
// Note that for security reasons, redirect rules are only processed when the request has origin isolation.
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
// See https://github.com/ipfs/specs/pull/290 for more information.
func (i *gatewayHandler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool, hadMatchingRule bool) {
redirectsFile := i.getRedirectsFile(r, contentPath, logger)
if redirectsFile != nil {
redirectRules, err := i.getRedirectRules(r, redirectsFile)
if err != nil {
internalWebError(w, err)
return nil, nil, false, true
}

redirected, newPath, err := i.handleRedirectsFileRules(w, r, contentPath, redirectRules)
if err != nil {
err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsFile.String(), err)
internalWebError(w, err)
return nil, nil, false, true
}

if redirected {
return nil, nil, false, true
}

// 200 is treated as a rewrite, so update the path and continue
if newPath != "" {
// Reassign contentPath and resolvedPath since the URL was rewritten
contentPath = ipath.New(newPath)
resolvedPath, err = i.api.ResolvePath(r.Context(), contentPath)
if err != nil {
internalWebError(w, err)
return nil, nil, false, true
}

return resolvedPath, contentPath, true, true
}
}
// No matching rule
return resolvedPath, contentPath, false, false
}

func (i *gatewayHandler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (bool, string, error) {
// Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite
pathParts := strings.Split(contentPath.String(), "/")
if len(pathParts) > 3 {
// All paths should start with /ipfs/cid/, so get the path after that
urlPath := "/" + strings.Join(pathParts[3:], "/")
rootPath := strings.Join(pathParts[:3], "/")
// Trim off the trailing /
urlPath = strings.TrimSuffix(urlPath, "/")

for _, rule := range redirectRules {
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
// Error right away if the rule is invalid
if !rule.MatchAndExpandPlaceholders(urlPath) {
continue
}

// We have a match!

// Rewrite
if rule.Status == 200 {
// Prepend the rootPath
toPath := rootPath + rule.To
return false, toPath, nil
}

// Or 400s
if rule.Status == 404 {
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
toPath := rootPath + rule.To
content404Path := ipath.New(toPath)
err := i.serve404(w, r, content404Path)
return true, toPath, err
}

if rule.Status == 410 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justincjohnson I missed these before:

  • 410 and 451 are missing sharness tests
  • 410 and 451 should behave similarly to what we do for 404, allowing for custom error pages to be rendered informing user why content was taken down.
    • perhaps reuse i.serve404 – rename it to i.serve4xx and make it accept specific code as a parameter?
  • coreiface.ErrResolveFailed should not be used for them, create dedicated errors

lmk if you have time to address this before Monday, ideally we would include examples of these in the text fixture that is listed in specs (sadly the CID needs to be updated once again).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. I'll get this done tomorrow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, please avoid force-push this time, will make review easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made the changes and pushed (not force!).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CIDs have been updated in the spec and IPIP as well.

webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), coreiface.ErrResolveFailed, http.StatusGone)
return true, rule.To, nil
}

if rule.Status == 451 {
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), coreiface.ErrResolveFailed, http.StatusUnavailableForLegalReasons)
return true, rule.To, nil
}

// Or redirect
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
if rule.Status >= 301 && rule.Status <= 308 {
http.Redirect(w, r, rule.To, rule.Status)
return true, rule.To, nil
}
}
}

// No redirects matched
return false, "", nil
}

func (i *gatewayHandler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) {
// Convert the path into a file node
node, err := i.api.Unixfs().Get(r.Context(), redirectsFilePath)
if err != nil {
return nil, fmt.Errorf("could not get _redirects node: %v", err)
}
defer node.Close()

// Convert the node into a file
f, ok := node.(files.File)
if !ok {
return nil, fmt.Errorf("could not parse _redirects: %v", err)
}

// Parse redirect rules from file
redirectRules, err := redirects.Parse(f)
if err != nil {
return nil, fmt.Errorf("could not parse _redirects: %v", err)
}

return redirectRules, nil
}

// Returns a resolved path to the _redirects file located in the root CID path of the requested path
func (i *gatewayHandler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved {
// contentPath is the full ipfs path to the requested resource,
// regardless of whether path or subdomain resolution is used.
rootPath := getRootPath(contentPath)

// Check for _redirects file.
// Any path resolution failures are ignored and we just assume there's no _redirects file.
// Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail.
path := ipath.Join(rootPath, "_redirects")
resolvedPath, err := i.api.ResolvePath(r.Context(), path)
if err != nil {
return nil
}
return resolvedPath
}

// Returns the root CID Path for the given path
func getRootPath(path ipath.Path) ipath.Path {
parts := strings.Split(path.String(), "/")
return ipath.New(gopath.Join("/", path.Namespace(), parts[2]))
}

func (i *gatewayHandler) serve404(w http.ResponseWriter, r *http.Request, content404Path ipath.Path) error {
resolved404Path, err := i.api.ResolvePath(r.Context(), content404Path)
if err != nil {
return err
}

node, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
if err != nil {
return err
}
defer node.Close()

f, ok := node.(files.File)
if !ok {
return fmt.Errorf("could not convert node for 404 page to file")
}

size, err := f.Size()
if err != nil {
return fmt.Errorf("could not get size of 404 page")
}

log.Debugw("using _redirects 404 file", "path", content404Path)
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
addCacheControlHeaders(w, r, content404Path, resolved404Path.Cid())
w.WriteHeader(http.StatusNotFound)
_, err = io.CopyN(w, f, size)
return err
}

func hasOriginIsolation(r *http.Request) bool {
_, gw := r.Context().Value(requestContextKey("gw-hostname")).(string)
_, dnslink := r.Context().Value("dnslink-hostname").(string)

if gw || dnslink {
return true
}

return false
}

func isUnixfsResponseFormat(responseFormat string) bool {
// The implicit response format is UnixFS
return responseFormat == ""
}
2 changes: 1 addition & 1 deletion core/corehttp/gateway_handler_unixfs_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit
var gwURL string

// Get gateway hostname and build gateway URL.
if h, ok := r.Context().Value("gw-hostname").(string); ok {
if h, ok := r.Context().Value(requestContextKey("gw-hostname")).(string); ok {
gwURL = "//" + h
} else {
gwURL = ""
Expand Down
7 changes: 5 additions & 2 deletions core/corehttp/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ func HostnameOption() ServeOption {
if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
childMux.ServeHTTP(w, withHostnameContext(r, host))
ctx := context.WithValue(r.Context(), requestContextKey("dnslink-hostname"), host)
childMux.ServeHTTP(w, withHostnameContext(r.WithContext(ctx), host))
return
}

Expand All @@ -242,6 +243,8 @@ type wildcardHost struct {
spec *config.GatewaySpec
}

type requestContextKey string

// Extends request context to include hostname of a canonical gateway root
// (subdomain root or dnslink fqdn)
func withHostnameContext(r *http.Request, hostname string) *http.Request {
Expand All @@ -250,7 +253,7 @@ func withHostnameContext(r *http.Request, hostname string) *http.Request {
// Host header, subdomain gateways have more comples rules (knownSubdomainDetails)
// More: https://github.com/ipfs/dir-index-html/issues/42
// nolint: staticcheck // non-backward compatible change
ctx := context.WithValue(r.Context(), "gw-hostname", hostname)
ctx := context.WithValue(r.Context(), requestContextKey("gw-hostname"), hostname)
return r.WithContext(ctx)
}

Expand Down
4 changes: 4 additions & 0 deletions docs/examples/kubo-as-a-library/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY
github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY=
github.com/ipfs/go-ipfs-provider v0.7.1 h1:eKToBUAb6ZY8iiA6AYVxzW4G1ep67XUaaEBUIYpxhfw=
github.com/ipfs/go-ipfs-provider v0.7.1/go.mod h1:QwdDYRYnC5sYGLlOwVDY/0ZB6T3zcMtu+5+GdGeUuw8=
github.com/ipfs/go-ipfs-redirects-file v0.1.0/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk=
github.com/ipfs/go-ipfs-routing v0.0.1/go.mod h1:k76lf20iKFxQTjcJokbPM9iBXVXVZhcOwc360N4nuKs=
github.com/ipfs/go-ipfs-routing v0.1.0/go.mod h1:hYoUkJLyAUKhF58tysKpids8RNDPO42BVMgK5dNsoqY=
github.com/ipfs/go-ipfs-routing v0.2.1 h1:E+whHWhJkdN9YeoHZNj5itzc+OR292AJ2uE9FFiW0BY=
Expand Down Expand Up @@ -1535,9 +1536,11 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
Expand Down Expand Up @@ -2179,6 +2182,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
Loading