/* Copyright The Helm Authors. 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 helm // import "k8s.io/helm/pkg/helm" import ( "fmt" "io" "time" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" healthpb "google.golang.org/grpc/health/grpc_health_v1" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" rls "k8s.io/helm/pkg/proto/hapi/services" ) // maxMsgSize use 20MB as the default message size limit. // grpc library default is 4MB const maxMsgSize = 1024 * 1024 * 20 // Client manages client side of the Helm-Tiller protocol. type Client struct { opts options } // NewClient creates a new client. func NewClient(opts ...Option) *Client { var c Client // set some sane defaults c.Option(ConnectTimeout(5)) return c.Option(opts...) } // Option configures the Helm client with the provided options. func (h *Client) Option(opts ...Option) *Client { for _, opt := range opts { opt(&h.opts) } return h } // ListReleases lists the current releases. func (h *Client) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.listReq ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.list(ctx, req) } // InstallRelease loads a chart from chstr, installs it, and returns the release response. func (h *Client) InstallRelease(chstr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { // load the chart to install chart, err := chartutil.Load(chstr) if err != nil { return nil, err } return h.InstallReleaseFromChart(chart, ns, opts...) } // InstallReleaseWithContext loads a chart from chstr, installs it, and returns the release response while accepting a context. func (h *Client) InstallReleaseWithContext(ctx context.Context, chstr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { // load the chart to install chart, err := chartutil.Load(chstr) if err != nil { return nil, err } return h.installReleaseFromChartWithContext(ctx, chart, ns, opts...) } // InstallReleaseFromChart installs a new chart and returns the release response. func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { return h.installReleaseFromChartWithContext(NewContext(), chart, ns, opts...) } // InstallReleaseFromChartWithContext installs a new chart and returns the release response while accepting a context. func (h *Client) InstallReleaseFromChartWithContext(ctx context.Context, chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { return h.installReleaseFromChartWithContext(ctx, chart, ns, opts...) } // InstallReleaseFromChartWithContext installs a new chart and returns the release response while accepting a context. func (h *Client) installReleaseFromChartWithContext(ctx context.Context, chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { // apply the install options reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.instReq req.Chart = chart req.Namespace = ns req.DryRun = reqOpts.dryRun req.DisableHooks = reqOpts.disableHooks req.DisableCrdHook = reqOpts.disableCRDHook req.ReuseName = reqOpts.reuseName ctx = FromContext(ctx) if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } err := chartutil.ProcessRequirementsEnabled(req.Chart, req.Values) if err != nil { return nil, err } err = chartutil.ProcessRequirementsImportValues(req.Chart) if err != nil { return nil, err } return h.install(ctx, req) } // DeleteRelease uninstalls a named release and returns the response. func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { // apply the uninstall options reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } if reqOpts.dryRun { // In the dry run case, just see if the release exists r, err := h.ReleaseContent(rlsName) if err != nil { return &rls.UninstallReleaseResponse{}, err } return &rls.UninstallReleaseResponse{Release: r.Release}, nil } req := &reqOpts.uninstallReq req.Name = rlsName req.DisableHooks = reqOpts.disableHooks ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.delete(ctx, req) } // UpdateRelease loads a chart from chstr and updates a release to a new/different chart. func (h *Client) UpdateRelease(rlsName string, chstr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { // load the chart to update chart, err := chartutil.Load(chstr) if err != nil { return nil, err } return h.UpdateReleaseFromChart(rlsName, chart, opts...) } // UpdateReleaseWithContext loads a chart from chstr and updates a release to a new/different chart while accepting a context. func (h *Client) UpdateReleaseWithContext(ctx context.Context, rlsName string, chstr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { // load the chart to update chart, err := chartutil.Load(chstr) if err != nil { return nil, err } return h.updateReleaseFromChartWithContext(ctx, rlsName, chart, opts...) } // UpdateReleaseFromChart updates a release to a new/different chart. func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { return h.updateReleaseFromChartWithContext(NewContext(), rlsName, chart, opts...) } // UpdateReleaseFromChartWithContext updates a release to a new/different chart while accepting a context. func (h *Client) UpdateReleaseFromChartWithContext(ctx context.Context, rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { return h.updateReleaseFromChartWithContext(ctx, rlsName, chart, opts...) } // updateReleaseFromChartWithContext updates a release to a new/different chart and accepts a context. func (h *Client) updateReleaseFromChartWithContext(ctx context.Context, rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { // apply the update options reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.updateReq req.Chart = chart req.DryRun = reqOpts.dryRun req.Name = rlsName req.DisableHooks = reqOpts.disableHooks req.Recreate = reqOpts.recreate req.Force = reqOpts.force req.ResetValues = reqOpts.resetValues req.ReuseValues = reqOpts.reuseValues ctx = FromContext(ctx) if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } err := chartutil.ProcessRequirementsEnabled(req.Chart, req.Values) if err != nil { return nil, err } err = chartutil.ProcessRequirementsImportValues(req.Chart) if err != nil { return nil, err } return h.update(ctx, req) } // GetVersion returns the server version. func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &rls.GetVersionRequest{} ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.version(ctx, req) } // RollbackRelease rolls back a release to the previous version. func (h *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.rollbackReq req.Recreate = reqOpts.recreate req.Force = reqOpts.force req.DisableHooks = reqOpts.disableHooks req.DryRun = reqOpts.dryRun req.Name = rlsName ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.rollback(ctx, req) } // ReleaseStatus returns the given release's status. func (h *Client) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.statusReq req.Name = rlsName ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.status(ctx, req) } // ReleaseContent returns the configuration for a given release. func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.contentReq req.Name = rlsName ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.content(ctx, req) } // ReleaseHistory returns a release's revision history. func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.histReq req.Name = rlsName ctx := NewContext() if reqOpts.before != nil { if err := reqOpts.before(ctx, req); err != nil { return nil, err } } return h.history(ctx, req) } // RunReleaseTest executes a pre-defined test on a release. func (h *Client) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) { reqOpts := h.opts for _, opt := range opts { opt(&reqOpts) } req := &reqOpts.testReq req.Name = rlsName ctx := NewContext() return h.test(ctx, req) } // PingTiller pings the Tiller pod and ensures that it is up and running func (h *Client) PingTiller() error { ctx := NewContext() return h.ping(ctx) } // connect returns a gRPC connection to Tiller or error. The gRPC dial options // are constructed here. func (h *Client) connect(ctx context.Context) (conn *grpc.ClientConn, err error) { opts := []grpc.DialOption{ grpc.WithBlock(), grpc.WithKeepaliveParams(keepalive.ClientParameters{ // Send keepalive every 30 seconds to prevent the connection from // getting closed by upstreams Time: time.Duration(30) * time.Second, }), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)), } switch { case h.opts.useTLS: opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(h.opts.tlsConfig))) default: opts = append(opts, grpc.WithInsecure()) } ctx, cancel := context.WithTimeout(ctx, h.opts.connectTimeout) defer cancel() if conn, err = grpc.DialContext(ctx, h.opts.host, opts...); err != nil { return nil, err } return conn, nil } // list executes tiller.ListReleases RPC. func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) s, err := rlc.ListReleases(ctx, req) if err != nil { return nil, err } var resp *rls.ListReleasesResponse for { r, err := s.Recv() if err == io.EOF { break } if err != nil { return nil, err } if resp == nil { resp = r continue } resp.Releases = append(resp.Releases, r.GetReleases()...) } return resp, nil } // install executes tiller.InstallRelease RPC. func (h *Client) install(ctx context.Context, req *rls.InstallReleaseRequest) (*rls.InstallReleaseResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.InstallRelease(ctx, req) } // delete executes tiller.UninstallRelease RPC. func (h *Client) delete(ctx context.Context, req *rls.UninstallReleaseRequest) (*rls.UninstallReleaseResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.UninstallRelease(ctx, req) } // update executes tiller.UpdateRelease RPC. func (h *Client) update(ctx context.Context, req *rls.UpdateReleaseRequest) (*rls.UpdateReleaseResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.UpdateRelease(ctx, req) } // rollback executes tiller.RollbackRelease RPC. func (h *Client) rollback(ctx context.Context, req *rls.RollbackReleaseRequest) (*rls.RollbackReleaseResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.RollbackRelease(ctx, req) } // status executes tiller.GetReleaseStatus RPC. func (h *Client) status(ctx context.Context, req *rls.GetReleaseStatusRequest) (*rls.GetReleaseStatusResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.GetReleaseStatus(ctx, req) } // content executes tiller.GetReleaseContent RPC. func (h *Client) content(ctx context.Context, req *rls.GetReleaseContentRequest) (*rls.GetReleaseContentResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.GetReleaseContent(ctx, req) } // version executes tiller.GetVersion RPC. func (h *Client) version(ctx context.Context, req *rls.GetVersionRequest) (*rls.GetVersionResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.GetVersion(ctx, req) } // history executes tiller.GetHistory RPC. func (h *Client) history(ctx context.Context, req *rls.GetHistoryRequest) (*rls.GetHistoryResponse, error) { c, err := h.connect(ctx) if err != nil { return nil, err } defer c.Close() rlc := rls.NewReleaseServiceClient(c) return rlc.GetHistory(ctx, req) } // test executes tiller.TestRelease RPC. func (h *Client) test(ctx context.Context, req *rls.TestReleaseRequest) (<-chan *rls.TestReleaseResponse, <-chan error) { errc := make(chan error, 1) c, err := h.connect(ctx) if err != nil { errc <- err return nil, errc } ch := make(chan *rls.TestReleaseResponse, 1) go func() { defer close(errc) defer close(ch) defer c.Close() rlc := rls.NewReleaseServiceClient(c) s, err := rlc.RunReleaseTest(ctx, req) if err != nil { errc <- err return } for { msg, err := s.Recv() if err == io.EOF { return } if err != nil { errc <- err return } ch <- msg } }() return ch, errc } // ping executes tiller.Ping RPC. func (h *Client) ping(ctx context.Context) error { c, err := h.connect(ctx) if err != nil { return err } defer c.Close() healthClient := healthpb.NewHealthClient(c) resp, err := healthClient.Check(ctx, &healthpb.HealthCheckRequest{Service: "Tiller"}) if err != nil { return err } switch resp.GetStatus() { case healthpb.HealthCheckResponse_SERVING: return nil case healthpb.HealthCheckResponse_NOT_SERVING: return fmt.Errorf("tiller is not serving requests at this time, Please try again later") default: return fmt.Errorf("tiller healthcheck returned an unknown status") } }