commit
323711f089
|
|
@ -26,6 +26,14 @@ type ImageRepositorySpec struct {
|
|||
// Image is the name of the image repository
|
||||
// +required
|
||||
Image string `json:"image,omitempty"`
|
||||
// ScanInterval is the (minimum) length of time to wait between
|
||||
// scans of the image repository.
|
||||
// +optional
|
||||
ScanInterval *metav1.Duration `json:"scanInterval,omitempty"`
|
||||
}
|
||||
|
||||
type ScanResult struct {
|
||||
TagCount int `json:"tagCount"`
|
||||
}
|
||||
|
||||
// ImageRepositoryStatus defines the observed state of ImageRepository
|
||||
|
|
@ -37,10 +45,17 @@ type ImageRepositoryStatus struct {
|
|||
// LastError is the error from last reconciliation, or empty if
|
||||
// reconciliation was successful.
|
||||
LastError string `json:"lastError"`
|
||||
// LastScanTime records the last time the repository was
|
||||
// successfully scanned.
|
||||
// +optional
|
||||
LastScanTime *metav1.Time `json:"lastScanTime,omitempty"`
|
||||
LastScanResult ScanResult `json:"lastScanResult,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Last scan",type=string,JSONPath=`.status.lastScanTime`
|
||||
// +kubebuilder:printcolumn:name="Tags",type=string,JSONPath=`.status.lastScanResult.tagCount`
|
||||
|
||||
// ImageRepository is the Schema for the imagerepositories API
|
||||
type ImageRepository struct {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
|
@ -29,8 +30,8 @@ func (in *ImageRepository) DeepCopyInto(out *ImageRepository) {
|
|||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
out.Status = in.Status
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepository.
|
||||
|
|
@ -86,6 +87,11 @@ func (in *ImageRepositoryList) DeepCopyObject() runtime.Object {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageRepositorySpec) DeepCopyInto(out *ImageRepositorySpec) {
|
||||
*out = *in
|
||||
if in.ScanInterval != nil {
|
||||
in, out := &in.ScanInterval, &out.ScanInterval
|
||||
*out = new(v1.Duration)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepositorySpec.
|
||||
|
|
@ -101,6 +107,11 @@ func (in *ImageRepositorySpec) DeepCopy() *ImageRepositorySpec {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageRepositoryStatus) DeepCopyInto(out *ImageRepositoryStatus) {
|
||||
*out = *in
|
||||
if in.LastScanTime != nil {
|
||||
in, out := &in.LastScanTime, &out.LastScanTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
out.LastScanResult = in.LastScanResult
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepositoryStatus.
|
||||
|
|
@ -112,3 +123,18 @@ func (in *ImageRepositoryStatus) DeepCopy() *ImageRepositoryStatus {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScanResult) DeepCopyInto(out *ScanResult) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScanResult.
|
||||
func (in *ScanResult) DeepCopy() *ScanResult {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScanResult)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ metadata:
|
|||
creationTimestamp: null
|
||||
name: imagerepositories.image.fluxcd.io
|
||||
spec:
|
||||
additionalPrinterColumns:
|
||||
- JSONPath: .status.lastScanTime
|
||||
name: Last scan
|
||||
type: string
|
||||
- JSONPath: .status.lastScanResult.tagCount
|
||||
name: Tags
|
||||
type: string
|
||||
group: image.fluxcd.io
|
||||
names:
|
||||
kind: ImageRepository
|
||||
|
|
@ -40,6 +47,10 @@ spec:
|
|||
image:
|
||||
description: Image is the name of the image repository
|
||||
type: string
|
||||
scanInterval:
|
||||
description: ScanInterval is the (minimum) length of time to wait between
|
||||
scans of the image repository.
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: ImageRepositoryStatus defines the observed state of ImageRepository
|
||||
|
|
@ -53,6 +64,18 @@ spec:
|
|||
description: LastError is the error from last reconciliation, or empty
|
||||
if reconciliation was successful.
|
||||
type: string
|
||||
lastScanResult:
|
||||
properties:
|
||||
tagCount:
|
||||
type: integer
|
||||
required:
|
||||
- tagCount
|
||||
type: object
|
||||
lastScanTime:
|
||||
description: LastScanTime records the last time the repository was successfully
|
||||
scanned.
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- lastError
|
||||
type: object
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ package controllers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
|
@ -28,6 +31,11 @@ import (
|
|||
imagev1alpha1 "github.com/squaremo/image-update/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
scanTimeout = 10 * time.Second
|
||||
defaultScanInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
// ImageRepositoryReconciler reconciles a ImageRepository object
|
||||
type ImageRepositoryReconciler struct {
|
||||
client.Client
|
||||
|
|
@ -60,13 +68,49 @@ func (r *ImageRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, er
|
|||
canonicalName := ref.Context().String()
|
||||
if imageRepo.Status.CanonicalImageName != canonicalName {
|
||||
imageRepo.Status.CanonicalImageName = canonicalName
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
ok, when := shouldScan(&imageRepo, now)
|
||||
if ok {
|
||||
ctx, cancel := context.WithTimeout(ctx, scanTimeout)
|
||||
defer cancel()
|
||||
tags, err := remote.ListWithContext(ctx, ref.Context()) // TODO: auth
|
||||
if err != nil {
|
||||
imageRepo.Status.LastError = err.Error()
|
||||
if err = r.Status().Update(ctx, &imageRepo); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
imageRepo.Status.LastScanTime = &metav1.Time{Time: now}
|
||||
imageRepo.Status.LastScanResult.TagCount = len(tags)
|
||||
imageRepo.Status.LastError = ""
|
||||
if err := r.Status().Update(ctx, &imageRepo); err != nil {
|
||||
log.Info("successful scan", "tag count", len(tags))
|
||||
if err = r.Status().Update(ctx, &imageRepo); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: when}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
// shouldScan takes an image repo and the time now, and says whether
|
||||
// the repository should be scanned now, and how long to wait for the
|
||||
// next scan.
|
||||
func shouldScan(repo *imagev1alpha1.ImageRepository, now time.Time) (bool, time.Duration) {
|
||||
scanInterval := defaultScanInterval
|
||||
if repo.Spec.ScanInterval != nil {
|
||||
scanInterval = repo.Spec.ScanInterval.Duration
|
||||
}
|
||||
|
||||
if repo.Status.LastScanTime == nil {
|
||||
return true, scanInterval
|
||||
}
|
||||
when := scanInterval - now.Sub(repo.Status.LastScanTime.Time)
|
||||
if when < time.Second {
|
||||
return true, scanInterval
|
||||
}
|
||||
return false, when
|
||||
}
|
||||
|
||||
func (r *ImageRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/random"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
imagev1alpha1 "github.com/squaremo/image-update/api/v1alpha1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// https://github.com/google/go-containerregistry/blob/v0.1.1/pkg/registry/compatibility_test.go
|
||||
// has an example of loading a test registry with a random image.
|
||||
|
||||
var _ = Describe("ImageRepository controller", func() {
|
||||
It("expands the canonical image name", func() {
|
||||
// would be good to test this without needing to do the scanning, since
|
||||
// 1. better to not rely on external services being available
|
||||
// 2. probably going to want to have several test cases
|
||||
repo := imagev1alpha1.ImageRepository{
|
||||
Spec: imagev1alpha1.ImageRepositorySpec{
|
||||
Image: "alpine",
|
||||
},
|
||||
}
|
||||
imageRepoName := types.NamespacedName{
|
||||
Name: "alpine-image",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
repo.Name = imageRepoName.Name
|
||||
repo.Namespace = imageRepoName.Namespace
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r := imageRepoReconciler
|
||||
err := r.Create(ctx, &repo)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var repoAfter imagev1alpha1.ImageRepository
|
||||
Eventually(func() bool {
|
||||
err := r.Get(context.Background(), imageRepoName, &repoAfter)
|
||||
return err == nil && repoAfter.Status.CanonicalImageName != ""
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(repoAfter.Name).To(Equal("alpine-image"))
|
||||
Expect(repoAfter.Namespace).To(Equal("default"))
|
||||
Expect(repoAfter.Status.CanonicalImageName).To(Equal("index.docker.io/library/alpine"))
|
||||
})
|
||||
|
||||
It("fetches the tags for an image", func() {
|
||||
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
|
||||
registry := strings.TrimPrefix(registryServer.URL, "http://")
|
||||
imgRepo := registry + "/myimage"
|
||||
for _, tag := range versions {
|
||||
imgRef, err := name.NewTag(imgRepo + ":" + tag)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
img, err := random.Image(512, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(remote.Write(imgRef, img)).To(Succeed())
|
||||
}
|
||||
|
||||
repo := imagev1alpha1.ImageRepository{
|
||||
Spec: imagev1alpha1.ImageRepositorySpec{
|
||||
Image: imgRepo,
|
||||
},
|
||||
}
|
||||
objectName := types.NamespacedName{
|
||||
Name: "random",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
repo.Name = objectName.Name
|
||||
repo.Namespace = objectName.Namespace
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r := imageRepoReconciler
|
||||
Expect(r.Create(ctx, &repo)).To(Succeed())
|
||||
|
||||
var repoAfter imagev1alpha1.ImageRepository
|
||||
Eventually(func() bool {
|
||||
err := r.Get(context.Background(), objectName, &repoAfter)
|
||||
return err == nil && repoAfter.Status.CanonicalImageName != ""
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(repoAfter.Status.CanonicalImageName).To(Equal(imgRepo))
|
||||
Expect(repoAfter.Status.LastScanResult.TagCount).To(Equal(len(versions)))
|
||||
})
|
||||
})
|
||||
|
|
@ -17,14 +17,17 @@ limitations under the License.
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
|
@ -53,6 +56,7 @@ var k8sClient client.Client
|
|||
var k8sMgr ctrl.Manager
|
||||
var imageRepoReconciler *ImageRepositoryReconciler
|
||||
var testEnv *envtest.Environment
|
||||
var registryServer *httptest.Server
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
|
@ -103,44 +107,68 @@ var _ = BeforeSuite(func(done Done) {
|
|||
k8sClient = k8sMgr.GetClient()
|
||||
Expect(k8sClient).ToNot(BeNil())
|
||||
|
||||
// set up a local registry for testing scanning
|
||||
regHandler := registry.New()
|
||||
registryServer = httptest.NewServer(&tagListHandler{
|
||||
registryHandler: regHandler,
|
||||
imagetags: map[string][]string{},
|
||||
})
|
||||
|
||||
close(done)
|
||||
}, 60)
|
||||
|
||||
var _ = Describe("ImageRepository controller", func() {
|
||||
It("expands the canonical image name", func() {
|
||||
repo := imagev1alpha1.ImageRepository{
|
||||
Spec: imagev1alpha1.ImageRepositorySpec{
|
||||
Image: "alpine",
|
||||
},
|
||||
}
|
||||
imageRepoName := types.NamespacedName{
|
||||
Name: "alpine-image",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
repo.Name = imageRepoName.Name
|
||||
repo.Namespace = imageRepoName.Namespace
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r := imageRepoReconciler
|
||||
err := r.Create(ctx, &repo)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var repoAfter imagev1alpha1.ImageRepository
|
||||
Eventually(func() bool {
|
||||
err = r.Get(context.Background(), imageRepoName, &repoAfter)
|
||||
return err == nil && repoAfter.Status.CanonicalImageName != ""
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(repoAfter.Name).To(Equal("alpine-image"))
|
||||
Expect(repoAfter.Namespace).To(Equal("default"))
|
||||
Expect(repoAfter.Status.CanonicalImageName).To(Equal("index.docker.io/library/alpine"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registryServer.Close()
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
// the go-containerregistry test regsitry implementation does not
|
||||
// serve /myimage/tags/list. Until it does, I'm adding this handler.
|
||||
// NB:
|
||||
// - assumes repo name is a single element
|
||||
// - assumes no overwriting tags
|
||||
|
||||
type tagListHandler struct {
|
||||
registryHandler http.Handler
|
||||
imagetags map[string][]string
|
||||
}
|
||||
|
||||
type tagListResult struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (h *tagListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// a tag list request has a path like: /v2/<repo>/tags/list
|
||||
if withoutTagsList := strings.TrimSuffix(r.URL.Path, "/tags/list"); r.Method == "GET" && withoutTagsList != r.URL.Path {
|
||||
repo := strings.TrimPrefix(withoutTagsList, "/v2/")
|
||||
if tags, ok := h.imagetags[repo]; ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
result := tagListResult{
|
||||
Name: repo,
|
||||
Tags: tags,
|
||||
}
|
||||
Expect(json.NewEncoder(w).Encode(result)).To(Succeed())
|
||||
println("Requested tags", repo, strings.Join(tags, ", "))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// record the fact of a PUT to a tag; the path looks like: /v2/<repo>/manifests/<tag>
|
||||
h.registryHandler.ServeHTTP(w, r)
|
||||
if r.Method == "PUT" {
|
||||
pathElements := strings.Split(r.URL.Path, "/")
|
||||
if len(pathElements) == 5 && pathElements[1] == "v2" && pathElements[3] == "manifests" {
|
||||
repo, tag := pathElements[2], pathElements[4]
|
||||
println("Recording tag", repo, tag)
|
||||
h.imagetags[repo] = append(h.imagetags[repo], tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -168,10 +168,13 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
|||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 h1:2HQmlpI3yI9deH18Q6xiSOIjXD4sLI55Y/gfpa8/558=
|
||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7 h1:Cvj7S8I4Xpx78KAl6TwTmMHuHlZ/0SM60NUneGJQ7IE=
|
||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
|
||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
|
|
@ -830,6 +833,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
|||
Loading…
Reference in New Issue