Skip to content

Commit 4117c59

Browse files
author
Andreas Fuchs
committed
Make the provider work, but in a slightly cursed way.
This actually manages to spin up a cluster link!
1 parent 5da0151 commit 4117c59

File tree

2 files changed

+140
-29
lines changed

2 files changed

+140
-29
lines changed

cmd/pulumi-resource-linkerd-link/main.go

Lines changed: 139 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
package main
1616

1717
import (
18-
"bytes"
1918
"context"
19+
"encoding/json"
2020
"fmt"
2121
"io/ioutil"
22+
"log"
23+
"os"
24+
"os/exec"
2225

2326
pbempty "github.com/golang/protobuf/ptypes/empty"
2427
multiclustercmd "github.com/linkerd/linkerd2/multicluster/cmd"
@@ -27,12 +30,33 @@ import (
2730
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
2831
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
2932
rpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
33+
"google.golang.org/protobuf/types/known/structpb"
3034
)
3135

3236
// Injected by linker in release builds.
3337
var version string
3438

39+
var linkerdInvocationArg = "--internal-only-invoke-linkerd-cli"
40+
3541
func main() {
42+
// Cursed code alert: Since linkerd's only public interface
43+
// for creating a multicluster link is currently to run the
44+
// CLI, and we don't want to require users to install the
45+
// linkerd binary (at least I don't), and linkerd doesn't
46+
// allow overriding its Stdout, here's what we do:
47+
//
48+
// When this program is invoked with
49+
// --internal-only-invoke-linkerd-cli, it acts as a "linkerd
50+
// multicluster" binary. Otherwise, it is a real pulumi
51+
// provider.
52+
//
53+
// TODO: Use real data structures when https://github.com/linkerd/linkerd2/pull/7335/files lands.
54+
if len(os.Args) > 1 && os.Args[1] == linkerdInvocationArg {
55+
if err := runMulticlusterLinkAsChild(os.Args[2:]); err != nil {
56+
log.Fatal(err)
57+
}
58+
return
59+
}
3660
err := provider.Main("linkerd-link", func(host *provider.HostClient) (rpc.ResourceProviderServer, error) {
3761
return &linkerdLinkProvider{
3862
host: host,
@@ -85,9 +109,38 @@ func (k *linkerdLinkProvider) Diff(ctx context.Context, req *rpc.DiffRequest) (*
85109
return nil, fmt.Errorf("Unknown resource type '%s'", ty)
86110
}
87111

88-
// TODO: Do work here.
112+
olds, err := plugin.UnmarshalProperties(req.GetOlds(), plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true})
113+
if err != nil {
114+
return nil, err
115+
}
116+
delete(olds, "repoDigest")
89117

90-
return &rpc.DiffResponse{}, nil
118+
news, err := plugin.UnmarshalProperties(req.GetNews(), plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true})
119+
if err != nil {
120+
return nil, err
121+
}
122+
d := olds.Diff(news)
123+
if d == nil {
124+
return &rpc.DiffResponse{
125+
Changes: rpc.DiffResponse_DIFF_NONE,
126+
}, nil
127+
}
128+
129+
diff := map[string]*rpc.PropertyDiff{}
130+
for key := range d.Adds {
131+
diff[string(key)] = &rpc.PropertyDiff{Kind: rpc.PropertyDiff_ADD}
132+
}
133+
for key := range d.Deletes {
134+
diff[string(key)] = &rpc.PropertyDiff{Kind: rpc.PropertyDiff_DELETE}
135+
}
136+
for key := range d.Updates {
137+
diff[string(key)] = &rpc.PropertyDiff{Kind: rpc.PropertyDiff_UPDATE}
138+
}
139+
return &rpc.DiffResponse{
140+
Changes: rpc.DiffResponse_DIFF_SOME,
141+
DetailedDiff: diff,
142+
HasDetailedDiff: true,
143+
}, nil
91144
}
92145

93146
func (k *linkerdLinkProvider) Create(ctx context.Context, req *rpc.CreateRequest) (*rpc.CreateResponse, error) {
@@ -98,24 +151,14 @@ func (k *linkerdLinkProvider) Create(ctx context.Context, req *rpc.CreateRequest
98151
}
99152

100153
props := req.GetProperties()
101-
inputs, err := plugin.UnmarshalProperties(props, plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true})
102-
103-
config, err := runMulticlusterLink([]string{
104-
"--context",
105-
inputs["from_cluster_kubeconfig"].StringValue(),
106-
"link",
107-
"--cluster-name",
108-
inputs["from_cluster_name"].StringValue(),
109-
})
154+
outputProperties, err := linkOtherCluster(props)
110155
if err != nil {
111156
return nil, err
112157
}
113-
plugin.MarshalProperties(
114-
resource.NewPropertyMapFromMap(map[string]interface{}{"config_group_yaml": config}),
115-
plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true},
116-
)
117-
118-
return &rpc.CreateResponse{}, nil
158+
return &rpc.CreateResponse{
159+
Id: "ignored",
160+
Properties: outputProperties,
161+
}, nil
119162
}
120163

121164
func (k *linkerdLinkProvider) Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) {
@@ -137,9 +180,14 @@ func (k *linkerdLinkProvider) Update(ctx context.Context, req *rpc.UpdateRequest
137180
return nil, fmt.Errorf("Unknown resource type '%s'", ty)
138181
}
139182

140-
// TODO: do work here.
141-
142-
return &rpc.UpdateResponse{}, nil
183+
props := req.GetNews()
184+
outputProperties, err := linkOtherCluster(props)
185+
if err != nil {
186+
return nil, err
187+
}
188+
return &rpc.UpdateResponse{
189+
Properties: outputProperties,
190+
}, nil
143191
}
144192

145193
func (k *linkerdLinkProvider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*pbempty.Empty, error) {
@@ -172,18 +220,80 @@ func (k *linkerdLinkProvider) Cancel(context.Context, *pbempty.Empty) (*pbempty.
172220
return &pbempty.Empty{}, nil
173221
}
174222

223+
// runMulticlusterLink runs this provider as a program that emulates
224+
// "linkerd multicluster", reading its output and reporting it as an
225+
// output property.
175226
func runMulticlusterLink(args []string) (string, error) {
176-
cmd := multiclustercmd.NewCmdMulticluster()
177-
cmd.SetArgs(args)
178-
b := bytes.NewBufferString("")
179-
cmd.SetOut(b)
180-
err := cmd.Execute()
227+
a := []string{linkerdInvocationArg}
228+
a = append(a, args...)
229+
cmd := exec.Command(os.Args[0], a...)
230+
p, err := cmd.StdoutPipe()
231+
if err != nil {
232+
return "", fmt.Errorf("setting up subprocess stdout as a pipe: %v", err)
233+
}
234+
errP, err := cmd.StderrPipe()
235+
if err != nil {
236+
return "", fmt.Errorf("setting up subprocess stderr as a pipe: %v", err)
237+
}
238+
err = cmd.Start()
181239
if err != nil {
182-
return "", err
240+
return "", fmt.Errorf("re-exec'ing self: %v", err)
183241
}
184-
out, err := ioutil.ReadAll(b)
242+
out, err := ioutil.ReadAll(p)
185243
if err != nil {
186-
return "", err
244+
return "", fmt.Errorf("reading linkerd multicluster output: %v", err)
245+
}
246+
err = cmd.Wait()
247+
if err != nil {
248+
stderr, _ := ioutil.ReadAll(errP)
249+
return "", fmt.Errorf("running linkerd multicluster as a subcommand: %v; %v", err, string(stderr))
187250
}
188251
return string(out), nil
189252
}
253+
254+
func runMulticlusterLinkAsChild(args []string) error {
255+
cmd := multiclustercmd.NewCmdMulticluster()
256+
cmd.SetArgs(args)
257+
cmd.SetOut(os.Stderr)
258+
return cmd.Execute()
259+
}
260+
261+
func linkOtherCluster(props *structpb.Struct) (*structpb.Struct, error) {
262+
inputs, err := plugin.UnmarshalProperties(props, plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true})
263+
264+
kubeconfigPath := ""
265+
kubecfgRaw := inputs["from_cluster_kubeconfig"]
266+
if kubecfgRaw.IsString() {
267+
kubecfgRaw.StringValue()
268+
} else {
269+
values := kubecfgRaw.ObjectValue().MapRepl(func(in string) (string, bool) {
270+
return in, true
271+
}, func(pv resource.PropertyValue) (interface{}, bool) {
272+
return pv, true
273+
})
274+
f, err := os.CreateTemp("", "kubeconfig")
275+
if err != nil {
276+
return nil, err
277+
}
278+
defer os.Remove(f.Name())
279+
kubeconfigPath = f.Name()
280+
enc := json.NewEncoder(f)
281+
if err = enc.Encode(values); err != nil {
282+
return nil, err
283+
}
284+
}
285+
config, err := runMulticlusterLink([]string{
286+
"--kubeconfig",
287+
kubeconfigPath,
288+
"link",
289+
"--cluster-name",
290+
inputs["from_cluster_name"].StringValue(),
291+
})
292+
if err != nil {
293+
return nil, err
294+
}
295+
return plugin.MarshalProperties(
296+
resource.NewPropertyMapFromMap(map[string]interface{}{"config_group_yaml": config}),
297+
plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true},
298+
)
299+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ require (
1414
github.com/pulumi/pulumi/sdk/v3 v3.0.0
1515
github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect
1616
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
17+
google.golang.org/protobuf v1.27.1
1718
)

0 commit comments

Comments
 (0)