Skip to content

Commit

Permalink
Merge pull request #763 from imeoer/nydusify-hook-plugin
Browse files Browse the repository at this point in the history
nydusify: support hook plugin
  • Loading branch information
jiangliu authored Sep 30, 2022
2 parents 87a6342 + aafe340 commit ac2b724
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 26 deletions.
1 change: 1 addition & 0 deletions contrib/nydusify/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ tmp
cmd/nydusify
output
nydusify-smoke
nydus-hook-plugin
5 changes: 4 additions & 1 deletion contrib/nydusify/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ifdef GOPROXY
PROXY := GOPROXY=${GOPROXY}
endif

.PHONY: all build release test clean build-smoke
.PHONY: all build release plugin test clean build-smoke

all: build

Expand All @@ -18,6 +18,9 @@ build:
release:
@CGO_ENABLED=0 ${PROXY} GOOS=linux GOARCH=${GOARCH} go build -ldflags '-X main.versionGitCommit=${GIT_COMMIT} -X main.versionBuildTime=${BUILD_TIME} -s -w -extldflags "-static"' -o ./cmd ./cmd/nydusify.go

plugin:
@CGO_ENABLED=0 ${PROXY} GOOS=linux GOARCH=${GOARCH} go build -ldflags '-s -w -extldflags "-static"' -o nydus-hook-plugin ./plugin

test: build build-smoke
@go vet $(PACKAGES)
golangci-lint run
Expand Down
23 changes: 16 additions & 7 deletions contrib/nydusify/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ require (
github.com/docker/distribution v2.8.1+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/google/uuid v1.2.0
github.com/hashicorp/go-hclog v1.3.1
github.com/hashicorp/go-plugin v1.4.5
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.3
github.com/prometheus/client_golang v1.11.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.7.2
github.com/urfave/cli/v2 v2.3.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
lukechampine.com/blake3 v1.1.5
)

Expand All @@ -34,14 +36,20 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v20.10.0-beta1.0.20201110211921-af34b94a78a1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.3 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/klauspost/compress v1.11.13 // indirect
github.com/klauspost/cpuid v1.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/mountinfo v0.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
Expand All @@ -50,12 +58,13 @@ require (
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/net v0.0.0-20220927171203-f486391704dc // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/opencontainers/runc => github.com/opencontainers/runc v1.1.2
59 changes: 41 additions & 18 deletions contrib/nydusify/go.sum

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions contrib/nydusify/pkg/converter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/backend"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/build"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/converter/provider"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/hook"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/metrics"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/remote"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/utils"
Expand Down Expand Up @@ -343,10 +344,20 @@ func (cvt *Converter) convert(ctx context.Context) (retErr error) {
})
}

// Ensure all blobs can be uploaded to storage backend.
if err := cvt.storageBackend.Finalize(false); err != nil {
return errors.Wrap(err, "Finalize backend upload")
}

// Call hook function before pushing manifest to registry.
info, err := cvt.newHookInfo(ctx, buildLayers)
if err != nil {
return errors.Wrap(err, "Get hook info")
}
if err := cvt.hookBeforePushManifest(ctx, info); err != nil {
return errors.Wrap(err, "Failed to call hook 'BeforePushManifest'")
}

// Push OCI manifest, Nydus manifest and manifest index
mm := &manifestManager{
sourceProvider: sourceProvider,
Expand All @@ -370,6 +381,11 @@ func (cvt *Converter) convert(ctx context.Context) (retErr error) {
}
pushDone(nil)

// Call hook function after pushing manifest to registry.
if err := cvt.hookAfterPushManifest(ctx, info); err != nil {
return errors.Wrap(err, "Failed to call hook 'AfterPushManifest'")
}

if repo != "" {
metrics.ConversionDuration(repo, len(sourceLayers), start)
}
Expand All @@ -392,6 +408,9 @@ func (cvt *Converter) convert(ctx context.Context) (retErr error) {

// Convert converts source image to target (Nydus) image
func (cvt *Converter) Convert(ctx context.Context) error {
hook.Init()
defer hook.Close()

if err := cvt.convert(ctx); err != nil {
if errors.Is(err, errInvalidCache) {
// Retry to convert without cache if the cache is invalid. we can't ensure the
Expand Down
86 changes: 86 additions & 0 deletions contrib/nydusify/pkg/converter/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2022 Nydus Developers. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package converter

import (
"context"
"path/filepath"

"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/hook"
"github.com/dragonflyoss/image-service/contrib/nydusify/pkg/utils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

func (cvt *Converter) newHookInfo(ctx context.Context, buildLayers []*buildLayer) (*hook.Info, error) {
blobs := []hook.Blob{}
for _, layer := range buildLayers {
record := layer.GetCacheRecord()
if record.NydusBlobDesc != nil {
blobs = append(blobs, hook.Blob{
ID: record.NydusBlobDesc.Digest.Hex(),
Size: record.NydusBlobDesc.Size,
})
}
}
bootstrapLayer := buildLayers[len(buildLayers)-1]
bootstrapPath := bootstrapLayer.bootstrapPath

if bootstrapPath == "" {
cache := bootstrapLayer.cacheGlue
// If we can't find bootstrap file in local, try to pull
// it from cache image.
if cache != nil && cache.remote != nil {
record := bootstrapLayer.GetCacheRecord()
reader, err := cache.remote.Pull(ctx, *record.NydusBootstrapDesc, true)
if err != nil {
return nil, errors.Wrap(err, "Pull cached bootstrap layer")
}
defer reader.Close()

bootstrapPath = filepath.Join(cvt.WorkDir, "bootstraps", "bootstrap_for_hook")
if err := utils.UnpackFile(reader, utils.BootstrapFileNameInLayer, bootstrapPath); err != nil {
return nil, errors.Wrap(err, "Unpack cached bootstrap layer")
}
}
}

info := hook.Info{
BootstrapPath: bootstrapPath,
SourceRef: cvt.SourceRemote.Ref,
TargetRef: cvt.TargetRemote.Ref,
Blobs: blobs,
}

return &info, nil
}

func (cvt *Converter) hookBeforePushManifest(ctx context.Context, info *hook.Info) error {
if hook.Caller == nil {
return nil
}

logrus.Info("[HOOK] Call hook 'BeforePushManifest'")

if err := hook.Caller.BeforePushManifest(info); err != nil {
return errors.Wrap(err, "Failed to call hook 'BeforePushManifest'")
}

return nil
}

func (cvt *Converter) hookAfterPushManifest(ctx context.Context, info *hook.Info) error {
if hook.Caller == nil {
return nil
}

logrus.Info("[HOOK] Call hook 'AfterPushManifest'")

if err := hook.Caller.AfterPushManifest(info); err != nil {
return errors.Wrap(err, "Failed to call hook 'AfterPushManifest'")
}

return nil
}
158 changes: 158 additions & 0 deletions contrib/nydusify/pkg/hook/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2022 Nydus Developers. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package hook

import (
"net/rpc"
"os"
"os/exec"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

var hookPluginPath = "./nydus-hook-plugin"

func init() {
envPath := os.Getenv("NYDUS_HOOK_PLUGIN_PATH")
if envPath != "" {
hookPluginPath = envPath
}
}

type Blob struct {
ID string `json:"id"`
Size int64 `json:"size"`
}

type Info struct {
BootstrapPath string `json:"bootstrap_path"`
SourceRef string `json:"source_ref"`
TargetRef string `json:"target_ref"`
Blobs []Blob `json:"blobs"`
}

type Hook interface {
BeforePushManifest(info *Info) error
AfterPushManifest(info *Info) error
}

type RPC struct{ client *rpc.Client }

func (h *RPC) BeforePushManifest(info *Info) error {
var resp error
err := h.client.Call("Plugin.BeforePushManifest", info, &resp)
if err != nil {
return err
}
return resp
}

func (h *RPC) AfterPushManifest(info *Info) error {
var resp error
err := h.client.Call("Plugin.AfterPushManifest", info, &resp)
if err != nil {
return err
}
return resp
}

type RPCServer struct {
Impl Hook
}

func (s *RPCServer) BeforePushManifest(info Info, resp *error) error {
*resp = s.Impl.BeforePushManifest(&info)
return *resp
}

func (s *RPCServer) AfterPushManifest(info Info, resp *error) error {
*resp = s.Impl.AfterPushManifest(&info)
return *resp
}

type Plugin struct {
Impl Hook
}

func (p *Plugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &RPCServer{Impl: p.Impl}, nil
}

func (Plugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &RPC{client: c}, nil
}

var Caller Hook

var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "NYDUS_HOOK_PLUGIN",
MagicCookieValue: "nydus-hook-plugin",
}

func NewPlugin(pluginImpl Hook) {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: map[string]plugin.Plugin{
"hook": &Plugin{Impl: pluginImpl},
},
})
}

var client *plugin.Client

func Init() {
if Caller != nil {
return
}

if _, err := os.Stat(hookPluginPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
return
}
logrus.Errorln(errors.Wrapf(err, "try load hook plugin %s", hookPluginPath))
return
}

var pluginMap = map[string]plugin.Plugin{
"hook": &Plugin{},
}

client = plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
Cmd: exec.Command(hookPluginPath),
Logger: hclog.New(&hclog.LoggerOptions{
Output: hclog.DefaultOutput,
Level: hclog.Error,
Name: "plugin",
}),
})

rpcClient, err := client.Client()
if err != nil {
logrus.WithError(err).Error("Failed to create rpc client")
return
}

raw, err := rpcClient.Dispense("hook")
if err != nil {
logrus.WithError(err).Error("Failed to dispense hook")
return
}

logrus.Infof("[HOOK] Loaded hook plugin %s", hookPluginPath)

Caller = raw.(Hook)
}

func Close() {
if client != nil {
defer client.Kill()
}
}
Loading

0 comments on commit ac2b724

Please sign in to comment.