1515package main
1616
1717import (
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.
3337var version string
3438
39+ var linkerdInvocationArg = "--internal-only-invoke-linkerd-cli"
40+
3541func 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
93146func (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
121164func (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
145193func (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.
175226func 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+ }
0 commit comments