From 5eb29e9ed9bf4792bd19bff7dd5ae364a51e138c Mon Sep 17 00:00:00 2001
From: Johnny Steenbergen <jonathansteenbergen@gmail.com>
Date: Wed, 6 Nov 2019 16:45:00 -0800
Subject: [PATCH] feat(pkger): add label associations to variables

---
 cmd/influx/pkg.go                             | 121 ++++++++-
 cmd/influxd/launcher/launcher.go              |   9 +-
 http/swagger.yml                              |  40 ++-
 pkger/models.go                               | 175 ++++++++----
 pkger/models_test.go                          |  14 +-
 pkger/parser.go                               |  11 +-
 pkger/parser_test.go                          |  59 ++++-
 pkger/service.go                              | 250 ++++++++++++++++--
 pkger/service_test.go                         | 240 ++++++++++++++---
 pkger/testdata/variables_associates_label.yml |  18 ++
 10 files changed, 809 insertions(+), 128 deletions(-)
 create mode 100644 pkger/testdata/variables_associates_label.yml

diff --git a/cmd/influx/pkg.go b/cmd/influx/pkg.go
index 71476937aa..eeebd00508 100644
--- a/cmd/influx/pkg.go
+++ b/cmd/influx/pkg.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"os"
@@ -17,7 +18,6 @@ import (
 	"github.com/olekukonko/tablewriter"
 	"github.com/spf13/cobra"
 	input "github.com/tcnksm/go-input"
-	"go.uber.org/zap"
 )
 
 func pkgCmd() *cobra.Command {
@@ -107,7 +107,17 @@ func newPkgerSVC(f Flags) (*pkger.Service, error) {
 		return nil, err
 	}
 
-	return pkger.NewService(zap.NewNop(), bucketSVC, labelSVC, dashSVC), nil
+	varSVC, err := newVariableService(f)
+	if err != nil {
+		return nil, err
+	}
+
+	return pkger.NewService(
+		pkger.WithBucketSVC(bucketSVC),
+		pkger.WithDashboardSVC(dashSVC),
+		pkger.WithLabelSVC(labelSVC),
+		pkger.WithVariableSVC(varSVC),
+	), nil
 }
 
 func newDashboardService(f Flags) (influxdb.DashboardService, error) {
@@ -130,6 +140,16 @@ func newLabelService(f Flags) (influxdb.LabelService, error) {
 	}, nil
 }
 
+func newVariableService(f Flags) (influxdb.VariableService, error) {
+	if f.local {
+		return newLocalKVService()
+	}
+	return &http.VariableService{
+		Addr:  f.host,
+		Token: f.token,
+	}, nil
+}
+
 func pkgFromFile(path string) (*pkger.Pkg, error) {
 	var enc pkger.Encoding
 	switch ext := filepath.Ext(path); ext {
@@ -184,9 +204,10 @@ func printPkgDiff(hasColor, hasTableBorders bool, diff pkger.Diff) {
 		return fmt.Sprintf("%s\n%s", red(o), green(n))
 	}
 
+	tablePrintFn := tablePrinterGen(hasColor, hasTableBorders)
 	if labels := diff.Labels; len(labels) > 0 {
 		headers := []string{"New", "ID", "Name", "Color", "Description"}
-		tablePrinter("LABELS", headers, len(labels), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("LABELS", headers, len(labels), func(w *tablewriter.Table) {
 			for _, l := range labels {
 				w.Append([]string{
 					boolDiff(l.IsNew()),
@@ -201,7 +222,7 @@ func printPkgDiff(hasColor, hasTableBorders bool, diff pkger.Diff) {
 
 	if bkts := diff.Buckets; len(bkts) > 0 {
 		headers := []string{"New", "ID", "Name", "Retention Period", "Description"}
-		tablePrinter("BUCKETS", headers, len(bkts), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("BUCKETS", headers, len(bkts), func(w *tablewriter.Table) {
 			for _, b := range bkts {
 				w.Append([]string{
 					boolDiff(b.IsNew()),
@@ -216,7 +237,7 @@ func printPkgDiff(hasColor, hasTableBorders bool, diff pkger.Diff) {
 
 	if dashes := diff.Dashboards; len(dashes) > 0 {
 		headers := []string{"New", "Name", "Description", "Num Charts"}
-		tablePrinter("DASHBOARDS", headers, len(dashes), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("DASHBOARDS", headers, len(dashes), func(w *tablewriter.Table) {
 			for _, d := range dashes {
 				w.Append([]string{
 					boolDiff(true),
@@ -228,9 +249,33 @@ func printPkgDiff(hasColor, hasTableBorders bool, diff pkger.Diff) {
 		})
 	}
 
+	if vars := diff.Variables; len(vars) > 0 {
+		headers := []string{"New", "ID", "Name", "Description", "Arg Type", "Arg Values"}
+		tablePrintFn("VARIABLES", headers, len(vars), func(w *tablewriter.Table) {
+			for _, v := range vars {
+				var oldArgType string
+				if v.OldArgs != nil {
+					oldArgType = v.OldArgs.Type
+				}
+				var newArgType string
+				if v.NewArgs != nil {
+					newArgType = v.NewArgs.Type
+				}
+				w.Append([]string{
+					boolDiff(v.IsNew()),
+					v.ID.String(),
+					v.Name,
+					strDiff(v.IsNew(), v.OldDesc, v.NewDesc),
+					strDiff(v.IsNew(), oldArgType, newArgType),
+					strDiff(v.IsNew(), printVarArgs(v.OldArgs), printVarArgs(v.NewArgs)),
+				})
+			}
+		})
+	}
+
 	if len(diff.LabelMappings) > 0 {
 		headers := []string{"New", "Resource Type", "Resource Name", "Resource ID", "Label Name", "Label ID"}
-		tablePrinter("LABEL MAPPINGS", headers, len(diff.LabelMappings), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("LABEL MAPPINGS", headers, len(diff.LabelMappings), func(w *tablewriter.Table) {
 			for _, m := range diff.LabelMappings {
 				w.Append([]string{
 					boolDiff(m.IsNew),
@@ -245,10 +290,43 @@ func printPkgDiff(hasColor, hasTableBorders bool, diff pkger.Diff) {
 	}
 }
 
+func printVarArgs(a *influxdb.VariableArguments) string {
+	if a == nil {
+		return "<nil>"
+	}
+	if a.Type == "map" {
+		b, err := json.Marshal(a.Values)
+		if err != nil {
+			return "{}"
+		}
+		return string(b)
+	}
+	if a.Type == "constant" {
+		vals, ok := a.Values.(influxdb.VariableConstantValues)
+		if !ok {
+			return "[]"
+		}
+		var out []string
+		for _, s := range vals {
+			out = append(out, fmt.Sprintf("%q", s))
+		}
+		return fmt.Sprintf("[%s]", strings.Join(out, " "))
+	}
+	if a.Type == "query" {
+		qVal, ok := a.Values.(influxdb.VariableQueryValues)
+		if !ok {
+			return ""
+		}
+		return fmt.Sprintf("language=%q query=%q", qVal.Language, qVal.Query)
+	}
+	return "unknown variable argument"
+}
+
 func printPkgSummary(hasColor, hasTableBorders bool, sum pkger.Summary) {
+	tablePrintFn := tablePrinterGen(hasColor, hasTableBorders)
 	if labels := sum.Labels; len(labels) > 0 {
 		headers := []string{"ID", "Name", "Description", "Color"}
-		tablePrinter("LABELS", headers, len(labels), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("LABELS", headers, len(labels), func(w *tablewriter.Table) {
 			for _, l := range labels {
 				w.Append([]string{
 					l.ID.String(),
@@ -262,7 +340,7 @@ func printPkgSummary(hasColor, hasTableBorders bool, sum pkger.Summary) {
 
 	if buckets := sum.Buckets; len(buckets) > 0 {
 		headers := []string{"ID", "Name", "Retention", "Description"}
-		tablePrinter("BUCKETS", headers, len(buckets), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("BUCKETS", headers, len(buckets), func(w *tablewriter.Table) {
 			for _, bucket := range buckets {
 				w.Append([]string{
 					bucket.ID.String(),
@@ -276,7 +354,7 @@ func printPkgSummary(hasColor, hasTableBorders bool, sum pkger.Summary) {
 
 	if dashes := sum.Dashboards; len(dashes) > 0 {
 		headers := []string{"ID", "Name", "Description"}
-		tablePrinter("DASHBOARDS", headers, len(dashes), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("DASHBOARDS", headers, len(dashes), func(w *tablewriter.Table) {
 			for _, d := range dashes {
 				w.Append([]string{
 					d.ID.String(),
@@ -287,9 +365,25 @@ func printPkgSummary(hasColor, hasTableBorders bool, sum pkger.Summary) {
 		})
 	}
 
+	if vars := sum.Variables; len(vars) > 0 {
+		headers := []string{"ID", "Name", "Description", "Arg Type", "Arg Values"}
+		tablePrintFn("VARIABLES", headers, len(vars), func(w *tablewriter.Table) {
+			for _, v := range vars {
+				args := v.Arguments
+				w.Append([]string{
+					v.ID.String(),
+					v.Name,
+					v.Description,
+					args.Type,
+					printVarArgs(args),
+				})
+			}
+		})
+	}
+
 	if mappings := sum.LabelMappings; len(mappings) > 0 {
 		headers := []string{"Resource Type", "Resource Name", "Resource ID", "Label Name", "Label ID"}
-		tablePrinter("LABEL MAPPINGS", headers, len(mappings), hasColor, hasTableBorders, func(w *tablewriter.Table) {
+		tablePrintFn("LABEL MAPPINGS", headers, len(mappings), func(w *tablewriter.Table) {
 			for _, m := range mappings {
 				w.Append([]string{
 					string(m.ResourceType),
@@ -303,6 +397,12 @@ func printPkgSummary(hasColor, hasTableBorders bool, sum pkger.Summary) {
 	}
 }
 
+func tablePrinterGen(hasColor, hasTableBorder bool) func(table string, headers []string, count int, appendFn func(w *tablewriter.Table)) {
+	return func(table string, headers []string, count int, appendFn func(w *tablewriter.Table)) {
+		tablePrinter(table, headers, count, hasColor, hasTableBorder, appendFn)
+	}
+}
+
 func tablePrinter(table string, headers []string, count int, hasColor, hasTableBorders bool, appendFn func(w *tablewriter.Table)) {
 	descrCol := -1
 	for i, h := range headers {
@@ -321,7 +421,6 @@ func tablePrinter(table string, headers []string, count int, hasColor, hasTableB
 		alignments = append(alignments, tablewriter.ALIGN_CENTER)
 	}
 	if descrCol != -1 {
-		w.SetAutoWrapText(false)
 		w.SetColMinWidth(descrCol, 30)
 		alignments[descrCol] = tablewriter.ALIGN_LEFT
 	}
diff --git a/cmd/influxd/launcher/launcher.go b/cmd/influxd/launcher/launcher.go
index 66e2e56cab..03b1b0aee9 100644
--- a/cmd/influxd/launcher/launcher.go
+++ b/cmd/influxd/launcher/launcher.go
@@ -839,9 +839,14 @@ func (m *Launcher) run(ctx context.Context) (err error) {
 
 	var pkgSVC pkger.SVC
 	{
-		pkgLogger := m.logger.With(zap.String("service", "pkger"))
 		b := m.apibackend
-		pkgSVC = pkger.NewService(pkgLogger, b.BucketService, b.LabelService, b.DashboardService)
+		pkgSVC = pkger.NewService(
+			pkger.WithLogger(m.logger.With(zap.String("service", "pkger"))),
+			pkger.WithBucketSVC(b.BucketService),
+			pkger.WithDashboardSVC(b.DashboardService),
+			pkger.WithLabelSVC(b.LabelService),
+			pkger.WithVariableSVC(b.VariableService),
+		)
 	}
 
 	var pkgHTTPServer *http.HandlerPkg
diff --git a/http/swagger.yml b/http/swagger.yml
index 80928cb33c..ff55dfd3b6 100644
--- a/http/swagger.yml
+++ b/http/swagger.yml
@@ -7186,6 +7186,17 @@ components:
                     type: string
                   labelID:
                     type: string
+            variables:
+              type: array
+              items:
+                allOf:
+                  - $ref: "#/components/schemas/Variable"
+                  - type: object
+                    properties:
+                      labelAssociations:
+                        type: array
+                        items:
+                          $ref: "#/components/schemas/Label"
         diff:
           type: object
           properties:
@@ -7253,6 +7264,23 @@ components:
                     type: string
                   labelName:
                     type: string
+            variables:
+              type: array
+              items:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  name:
+                    type: string
+                  oldDescription:
+                    type: string
+                  newDescription:
+                    type: string
+                  oldArgs:
+                    $ref: "#/components/schemas/VariableProperties"
+                  newArgs:
+                    $ref: "#/components/schemas/VariableProperties"
     PkgChart:
       type: object
       properties:
@@ -8461,11 +8489,7 @@ components:
         labels:
           $ref: "#/components/schemas/Labels"
         arguments:
-          type: object
-          oneOf:
-            - $ref: "#/components/schemas/QueryVariableProperties"
-            - $ref: "#/components/schemas/ConstantVariableProperties"
-            - $ref: "#/components/schemas/MapVariableProperties"
+          $ref: "#/components/schemas/VariableProperties"
         createdAt:
           type: string
           format: date-time
@@ -8511,6 +8535,12 @@ components:
           type: array
           items:
             $ref: "#/components/schemas/Variable"
+    VariableProperties:
+      type: object
+      oneOf:
+        - $ref: "#/components/schemas/QueryVariableProperties"
+        - $ref: "#/components/schemas/ConstantVariableProperties"
+        - $ref: "#/components/schemas/MapVariableProperties"
     ViewProperties:
       oneOf:
         - $ref: "#/components/schemas/LinePlusSingleStatProperties"
diff --git a/pkger/models.go b/pkger/models.go
index 6644e4ae90..eb9e6529be 100644
--- a/pkger/models.go
+++ b/pkger/models.go
@@ -67,6 +67,7 @@ type Diff struct {
 	Dashboards    []DiffDashboard    `json:"dashboards"`
 	Labels        []DiffLabel        `json:"labels"`
 	LabelMappings []DiffLabelMapping `json:"labelMappings"`
+	Variables     []DiffVariable     `json:"variables"`
 }
 
 // DiffBucket is a diff of an individual bucket.
@@ -163,6 +164,33 @@ type DiffLabelMapping struct {
 	LabelName string `json:"labelName"`
 }
 
+// DiffVariable is a diff of an individual variable.
+type DiffVariable struct {
+	ID      SafeID `json:"id"`
+	Name    string `json:"name"`
+	OldDesc string `json:"oldDescription"`
+	NewDesc string `json:"newDescription"`
+
+	OldArgs *influxdb.VariableArguments `json:"oldArgs"`
+	NewArgs *influxdb.VariableArguments `json:"newArgs"`
+}
+
+func newDiffVariable(v *variable, iv influxdb.Variable) DiffVariable {
+	return DiffVariable{
+		ID:      SafeID(iv.ID),
+		Name:    v.Name,
+		OldDesc: iv.Description,
+		NewDesc: v.Description,
+		OldArgs: iv.Arguments,
+		NewArgs: v.influxVarArgs(),
+	}
+}
+
+// IsNew indicates whether a pkg variable is going to be new to the platform.
+func (d DiffVariable) IsNew() bool {
+	return d.ID == SafeID(0)
+}
+
 // Summary is a definition of all the resources that have or
 // will be created from a pkg.
 type Summary struct {
@@ -240,6 +268,7 @@ type SummaryLabelMapping struct {
 // SummaryVariable provides a summary of a pkg variable.
 type SummaryVariable struct {
 	influxdb.Variable
+	LabelAssociations []influxdb.Label `json:"labelAssociations"`
 }
 
 type bucket struct {
@@ -317,14 +346,68 @@ func (l assocMapVal) dashboard() (*dashboard, bool) {
 	return d, ok
 }
 
+func (l assocMapVal) variable() (*variable, bool) {
+	if l.v == nil {
+		return nil, false
+	}
+	v, ok := l.v.(*variable)
+	return v, ok
+}
+
+type associationMapping struct {
+	mappings map[assocMapKey]assocMapVal
+}
+
+func (l *associationMapping) setMapping(k assocMapKey, v assocMapVal) {
+	if l == nil {
+		return
+	}
+	if l.mappings == nil {
+		l.mappings = make(map[assocMapKey]assocMapVal)
+	}
+	l.mappings[k] = v
+}
+
+func (l *associationMapping) setBucketMapping(b *bucket, exists bool) {
+	key := assocMapKey{
+		resType: b.ResourceType(),
+		name:    b.Name,
+	}
+	val := assocMapVal{
+		exists: exists,
+		v:      b,
+	}
+	l.setMapping(key, val)
+}
+
+func (l *associationMapping) setDashboardMapping(d *dashboard) {
+	key := assocMapKey{
+		resType: d.ResourceType(),
+		name:    d.Name,
+	}
+	val := assocMapVal{v: d}
+	l.setMapping(key, val)
+}
+
+func (l *associationMapping) setVariableMapping(v *variable, exists bool) {
+	key := assocMapKey{
+		resType: v.ResourceType(),
+		name:    v.Name,
+	}
+	val := assocMapVal{
+		exists: exists,
+		v:      v,
+	}
+	l.setMapping(key, val)
+}
+
 type label struct {
 	id          influxdb.ID
 	OrgID       influxdb.ID
 	Name        string
 	Color       string
 	Description string
-
-	mappings map[assocMapKey]assocMapVal
+	associationMapping
 
 	// exists provides context for a resource that already
 	// exists in the platform. If a resource already exists(exists=true)
@@ -387,43 +470,15 @@ func (l *label) getMappedResourceID(k assocMapKey) influxdb.ID {
 		if ok {
 			return d.ID()
 		}
+	case influxdb.VariablesResourceType:
+		v, ok := l.mappings[k].variable()
+		if ok {
+			return v.ID()
+		}
 	}
 	return 0
 }
 
-func (l *label) setBucketMapping(b *bucket, exists bool) {
-	if l == nil {
-		return
-	}
-	if l.mappings == nil {
-		l.mappings = make(map[assocMapKey]assocMapVal)
-	}
-
-	key := assocMapKey{
-		resType: influxdb.BucketsResourceType,
-		name:    b.Name,
-	}
-	l.mappings[key] = assocMapVal{
-		exists: exists,
-		v:      b,
-	}
-}
-
-func (l *label) setDashboardMapping(d *dashboard) {
-	if l == nil {
-		return
-	}
-	if l.mappings == nil {
-		l.mappings = make(map[assocMapKey]assocMapVal)
-	}
-
-	key := assocMapKey{
-		resType: d.ResourceType(),
-		name:    d.Name,
-	}
-	l.mappings[key] = assocMapVal{v: d}
-}
-
 func (l *label) properties() map[string]string {
 	return map[string]string{
 		"color":       l.Color,
@@ -455,10 +510,47 @@ type variable struct {
 	ConstValues []string
 	MapValues   map[string]string
 
-	mappings map[assocMapKey]assocMapVal
+	labels []*label
+
+	existing *influxdb.Variable
+}
+
+func (v *variable) ID() influxdb.ID {
+	if v.existing != nil {
+		return v.existing.ID
+	}
+	return v.id
+}
+
+func (v *variable) Exists() bool {
+	return v.existing != nil
+}
+
+func (v *variable) ResourceType() influxdb.ResourceType {
+	return influxdb.VariablesResourceType
+}
+
+func (v *variable) shouldApply() bool {
+	return v.existing == nil ||
+		v.existing.Description != v.Description ||
+		v.existing.Arguments == nil ||
+		v.existing.Arguments.Type != v.Type
 }
 
 func (v *variable) summarize() SummaryVariable {
+	return SummaryVariable{
+		Variable: influxdb.Variable{
+			ID:             v.ID(),
+			OrganizationID: v.OrgID,
+			Name:           v.Name,
+			Description:    v.Description,
+			Arguments:      v.influxVarArgs(),
+		},
+		LabelAssociations: toInfluxLabels(v.labels...),
+	}
+}
+
+func (v *variable) influxVarArgs() *influxdb.VariableArguments {
 	args := &influxdb.VariableArguments{
 		Type: v.Type,
 	}
@@ -473,16 +565,7 @@ func (v *variable) summarize() SummaryVariable {
 	case "map":
 		args.Values = influxdb.VariableMapValues(v.MapValues)
 	}
-
-	return SummaryVariable{
-		Variable: influxdb.Variable{
-			ID:             v.id,
-			OrganizationID: v.OrgID,
-			Name:           v.Name,
-			Description:    v.Description,
-			Arguments:      args,
-		},
-	}
+	return args
 }
 
 func (v *variable) valid() []failure {
diff --git a/pkger/models_test.go b/pkger/models_test.go
index 45e88e92b7..639574c332 100644
--- a/pkger/models_test.go
+++ b/pkger/models_test.go
@@ -94,12 +94,14 @@ func TestPkg(t *testing.T) {
 				Name:        "name2",
 				Description: "desc2",
 				Color:       "blurple",
-				mappings: map[assocMapKey]assocMapVal{
-					assocMapKey{
-						resType: influxdb.BucketsResourceType,
-						name:    bucket1.Name,
-					}: {
-						v: bucket1,
+				associationMapping: associationMapping{
+					mappings: map[assocMapKey]assocMapVal{
+						assocMapKey{
+							resType: influxdb.BucketsResourceType,
+							name:    bucket1.Name,
+						}: {
+							v: bucket1,
+						},
 					},
 				},
 			}
diff --git a/pkger/parser.go b/pkger/parser.go
index 8388beb467..305bba2eae 100644
--- a/pkger/parser.go
+++ b/pkger/parser.go
@@ -513,6 +513,15 @@ func (p *Pkg) graphVariables() error {
 			MapValues:   r.mapStrStr("values"),
 		}
 
+		failures := p.parseNestedLabels(r, func(l *label) error {
+			newVar.labels = append(newVar.labels, l)
+			p.mLabels[l.Name].setVariableMapping(newVar, false)
+			return nil
+		})
+		sort.Slice(newVar.labels, func(i, j int) bool {
+			return newVar.labels[i].Name < newVar.labels[j].Name
+		})
+
 		p.mVariables[r.Name()] = newVar
 
 		// here we set the var on the var map and return fails
@@ -523,7 +532,7 @@ func (p *Pkg) graphVariables() error {
 		// invalid. So the mapping is correct. So we keep this
 		// to validate that mapping is correct, and return fails
 		// to indicate fails from the var.
-		return newVar.valid()
+		return append(failures, newVar.valid()...)
 	})
 }
 
diff --git a/pkger/parser_test.go b/pkger/parser_test.go
index 9b6d5fa5ee..65d7d2cae1 100644
--- a/pkger/parser_test.go
+++ b/pkger/parser_test.go
@@ -1,6 +1,8 @@
 package pkger
 
 import (
+	"path/filepath"
+	"strings"
 	"testing"
 	"time"
 
@@ -1831,6 +1833,47 @@ spec:
 			}
 		})
 	})
+
+	t.Run("pkg with variable and labels associated", func(t *testing.T) {
+		testfileRunner(t, "testdata/variables_associates_label.yml", func(t *testing.T, pkg *Pkg) {
+			sum := pkg.Summary()
+			require.Len(t, sum.Labels, 1)
+
+			vars := sum.Variables
+			require.Len(t, vars, 1)
+
+			expectedLabelMappings := []struct {
+				varName string
+				labels  []string
+			}{
+				{
+					varName: "var_1",
+					labels:  []string{"label_1"},
+				},
+			}
+			for i, expected := range expectedLabelMappings {
+				v := vars[i]
+				require.Len(t, v.LabelAssociations, len(expected.labels))
+
+				for j, label := range expected.labels {
+					assert.Equal(t, label, v.LabelAssociations[j].Name)
+				}
+			}
+
+			expectedMappings := []SummaryLabelMapping{
+				{
+					ResourceName: "var_1",
+					LabelName:    "label_1",
+				},
+			}
+
+			require.Len(t, sum.LabelMappings, len(expectedMappings))
+			for i, expected := range expectedMappings {
+				expected.LabelMapping.ResourceType = influxdb.VariablesResourceType
+				assert.Equal(t, expected, sum.LabelMappings[i])
+			}
+		})
+	})
 }
 
 type testPkgResourceError struct {
@@ -1925,21 +1968,31 @@ func testfileRunner(t *testing.T, path string, testFn func(t *testing.T, pkg *Pk
 	}{
 		{
 			name:      "yaml",
-			extension: "yml",
+			extension: ".yml",
 			encoding:  EncodingYAML,
 		},
 		{
 			name:      "json",
-			extension: "json",
+			extension: ".json",
 			encoding:  EncodingJSON,
 		},
 	}
 
+	ext := filepath.Ext(path)
+	switch ext {
+	case ".yml":
+		tests = tests[:1]
+	case ".json":
+		tests = tests[1:]
+	}
+
+	path = strings.TrimSuffix(path, ext)
+
 	for _, tt := range tests {
 		fn := func(t *testing.T) {
 			t.Helper()
 
-			pkg := validParsedPkg(t, path+"."+tt.extension, tt.encoding, baseAsserts{
+			pkg := validParsedPkg(t, path+tt.extension, tt.encoding, baseAsserts{
 				version:     "0.1.0",
 				kind:        "Package",
 				description: "pack description",
diff --git a/pkger/service.go b/pkger/service.go
index c8cdf84ab7..b81f162d0e 100644
--- a/pkger/service.go
+++ b/pkger/service.go
@@ -22,6 +22,53 @@ type SVC interface {
 	Apply(ctx context.Context, orgID influxdb.ID, pkg *Pkg) (Summary, error)
 }
 
+type serviceOpt struct {
+	logger *zap.Logger
+
+	labelSVC  influxdb.LabelService
+	bucketSVC influxdb.BucketService
+	dashSVC   influxdb.DashboardService
+	varSVC    influxdb.VariableService
+}
+
+// ServiceSetterFn is a means of setting dependencies on the Service type.
+type ServiceSetterFn func(opt *serviceOpt)
+
+// WithLogger sets the service logger.
+func WithLogger(logger *zap.Logger) ServiceSetterFn {
+	return func(opt *serviceOpt) {
+		opt.logger = logger
+	}
+}
+
+// WithBucketSVC sets the bucket service.
+func WithBucketSVC(bktSVC influxdb.BucketService) ServiceSetterFn {
+	return func(opt *serviceOpt) {
+		opt.bucketSVC = bktSVC
+	}
+}
+
+// WithDashboardSVC sets the dashboard service.
+func WithDashboardSVC(dashSVC influxdb.DashboardService) ServiceSetterFn {
+	return func(opt *serviceOpt) {
+		opt.dashSVC = dashSVC
+	}
+}
+
+// WithLabelSVC sets the label service.
+func WithLabelSVC(labelSVC influxdb.LabelService) ServiceSetterFn {
+	return func(opt *serviceOpt) {
+		opt.labelSVC = labelSVC
+	}
+}
+
+// WithVariableSVC sets the variable service.
+func WithVariableSVC(varSVC influxdb.VariableService) ServiceSetterFn {
+	return func(opt *serviceOpt) {
+		opt.varSVC = varSVC
+	}
+}
+
 // Service provides the pkger business logic including all the dependencies to make
 // this resource sausage.
 type Service struct {
@@ -30,21 +77,25 @@ type Service struct {
 	labelSVC  influxdb.LabelService
 	bucketSVC influxdb.BucketService
 	dashSVC   influxdb.DashboardService
+	varSVC    influxdb.VariableService
 }
 
 // NewService is a constructor for a pkger Service.
-func NewService(l *zap.Logger, bucketSVC influxdb.BucketService, labelSVC influxdb.LabelService, dashSVC influxdb.DashboardService) *Service {
-	svc := Service{
-		logger:    zap.NewNop(),
-		bucketSVC: bucketSVC,
-		labelSVC:  labelSVC,
-		dashSVC:   dashSVC,
+func NewService(opts ...ServiceSetterFn) *Service {
+	opt := &serviceOpt{
+		logger: zap.NewNop(),
+	}
+	for _, o := range opts {
+		o(opt)
 	}
 
-	if l != nil {
-		svc.logger = l
+	return &Service{
+		logger:    opt.logger,
+		bucketSVC: opt.bucketSVC,
+		labelSVC:  opt.labelSVC,
+		dashSVC:   opt.dashSVC,
+		varSVC:    opt.varSVC,
 	}
-	return &svc
 }
 
 // CreatePkgSetFn is a functional input for setting the pkg fields.
@@ -105,6 +156,11 @@ func (s *Service) DryRun(ctx context.Context, orgID influxdb.ID, pkg *Pkg) (Summ
 		return Summary{}, Diff{}, err
 	}
 
+	diffVars, err := s.dryRunVariables(ctx, orgID, pkg)
+	if err != nil {
+		return Summary{}, Diff{}, err
+	}
+
 	diffLabelMappings, err := s.dryRunLabelMappings(ctx, pkg)
 	if err != nil {
 		return Summary{}, Diff{}, err
@@ -120,6 +176,7 @@ func (s *Service) DryRun(ctx context.Context, orgID influxdb.ID, pkg *Pkg) (Summ
 		Dashboards:    diffDashes,
 		Labels:        diffLabels,
 		LabelMappings: diffLabelMappings,
+		Variables:     diffVars,
 	}
 	return pkg.Summary(), diff, nil
 }
@@ -169,9 +226,9 @@ func (s *Service) dryRunLabels(ctx context.Context, orgID influxdb.ID, pkg *Pkg)
 	mExistingLabels := make(map[string]DiffLabel)
 	labels := pkg.labels()
 	for i := range labels {
-		l := labels[i]
+		pkgLabel := labels[i]
 		existingLabels, err := s.labelSVC.FindLabels(ctx, influxdb.LabelFilter{
-			Name:  l.Name,
+			Name:  pkgLabel.Name,
 			OrgID: &orgID,
 		}, influxdb.FindOptions{Limit: 1})
 		switch {
@@ -179,10 +236,10 @@ func (s *Service) dryRunLabels(ctx context.Context, orgID influxdb.ID, pkg *Pkg)
 		//  err isn't a not found (some other error)
 		case err == nil && len(existingLabels) > 0:
 			existingLabel := existingLabels[0]
-			l.existing = existingLabel
-			mExistingLabels[l.Name] = newDiffLabel(l, *existingLabel)
+			pkgLabel.existing = existingLabel
+			mExistingLabels[pkgLabel.Name] = newDiffLabel(pkgLabel, *existingLabel)
 		default:
-			mExistingLabels[l.Name] = newDiffLabel(l, influxdb.Label{})
+			mExistingLabels[pkgLabel.Name] = newDiffLabel(pkgLabel, influxdb.Label{})
 		}
 	}
 
@@ -197,6 +254,49 @@ func (s *Service) dryRunLabels(ctx context.Context, orgID influxdb.ID, pkg *Pkg)
 	return diffs, nil
 }
 
+func (s *Service) dryRunVariables(ctx context.Context, orgID influxdb.ID, pkg *Pkg) ([]DiffVariable, error) {
+	mExistingLabels := make(map[string]DiffVariable)
+	variables := pkg.variables()
+
+VarLoop:
+	for i := range variables {
+		pkgVar := variables[i]
+		existingLabels, err := s.varSVC.FindVariables(ctx, influxdb.VariableFilter{
+			OrganizationID: &orgID,
+			// TODO: would be ideal to extend find variables to allow for a name matcher
+			//  since names are unique for vars within an org, meanwhile, make large limit
+			// 	returned vars, should be more than enough for the time being.
+		}, influxdb.FindOptions{Limit: 10000})
+		switch {
+		case err == nil && len(existingLabels) > 0:
+			for i := range existingLabels {
+				existingVar := existingLabels[i]
+				if existingVar.Name != pkgVar.Name {
+					continue
+				}
+				pkgVar.existing = existingVar
+				mExistingLabels[pkgVar.Name] = newDiffVariable(pkgVar, *existingVar)
+				continue VarLoop
+			}
+			// fallthrough here for when the variable is not found, it'll fall to the
+			// default case and add it as new.
+			fallthrough
+		default:
+			mExistingLabels[pkgVar.Name] = newDiffVariable(pkgVar, influxdb.Variable{})
+		}
+	}
+
+	diffs := make([]DiffVariable, 0, len(mExistingLabels))
+	for _, diff := range mExistingLabels {
+		diffs = append(diffs, diff)
+	}
+	sort.Slice(diffs, func(i, j int) bool {
+		return diffs[i].Name < diffs[j].Name
+	})
+
+	return diffs, nil
+}
+
 type (
 	labelMappingDiffFn func(labelID influxdb.ID, labelName string, isNew bool)
 
@@ -243,6 +343,23 @@ func (s *Service) dryRunLabelMappings(ctx context.Context, pkg *Pkg) ([]DiffLabe
 		}
 	}
 
+	for _, v := range pkg.variables() {
+		err := s.dryRunResourceLabelMapping(ctx, v, v.labels, func(labelID influxdb.ID, labelName string, isNew bool) {
+			pkg.mLabels[labelName].setVariableMapping(v, !isNew)
+			diffs = append(diffs, DiffLabelMapping{
+				IsNew:     isNew,
+				ResType:   v.ResourceType(),
+				ResID:     SafeID(v.ID()),
+				ResName:   v.Name,
+				LabelID:   SafeID(labelID),
+				LabelName: labelName,
+			})
+		})
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	// sort by res type ASC, then res name ASC, then label name ASC
 	sort.Slice(diffs, func(i, j int) bool {
 		n, m := diffs[i], diffs[j]
@@ -271,6 +388,7 @@ func (s *Service) dryRunResourceLabelMapping(ctx context.Context, la labelAssoci
 		}
 		return nil
 	}
+
 	// loop through and hit api for all labels associated with a bkt
 	// lookup labels in pkg, add it to the label mapping, if exists in
 	// the results from API, mark it exists
@@ -338,6 +456,7 @@ func (s *Service) Apply(ctx context.Context, orgID influxdb.ID, pkg *Pkg) (sum S
 		{
 			// primary resources
 			s.applyLabels(pkg.labels()),
+			s.applyVariables(pkg.variables()),
 			s.applyBuckets(pkg.buckets()),
 			s.applyDashboards(pkg.dashboards()),
 		},
@@ -589,7 +708,17 @@ func (s *Service) applyLabels(labels []*label) applier {
 func (s *Service) rollbackLabels(labels []*label) error {
 	var errs []string
 	for _, l := range labels {
-		err := s.labelSVC.DeleteLabel(context.Background(), l.ID())
+		if l.existing == nil {
+			err := s.labelSVC.DeleteLabel(context.Background(), l.ID())
+			if err != nil {
+				errs = append(errs, l.ID().String())
+			}
+			continue
+		}
+
+		_, err := s.labelSVC.UpdateLabel(context.Background(), l.ID(), influxdb.LabelUpdate{
+			Properties: l.existing.Properties,
+		})
 		if err != nil {
 			errs = append(errs, l.ID().String())
 		}
@@ -626,6 +755,97 @@ func (s *Service) applyLabel(ctx context.Context, l *label) (influxdb.Label, err
 	return influxLabel, nil
 }
 
+func (s *Service) applyVariables(vars []*variable) applier {
+	const resource = "variable"
+
+	rollBackVars := make([]*variable, 0, len(vars))
+	createFn := func(ctx context.Context, orgID influxdb.ID) error {
+		ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
+		defer cancel()
+
+		var errs applyErrs
+		for i, v := range vars {
+			vars[i].OrgID = orgID
+			if !v.shouldApply() {
+				continue
+			}
+			influxVar, err := s.applyVariable(ctx, v)
+			if err != nil {
+				errs = append(errs, applyErrBody{
+					name: v.Name,
+					msg:  err.Error(),
+				})
+				continue
+			}
+			vars[i].id = influxVar.ID
+			rollBackVars = append(rollBackVars, vars[i])
+		}
+
+		return errs.toError(resource, "failed to create variable")
+	}
+
+	return applier{
+		creater: createFn,
+		rollbacker: rollbacker{
+			resource: resource,
+			fn:       func() error { return s.rollbackVariables(rollBackVars) },
+		},
+	}
+}
+
+func (s *Service) rollbackVariables(variables []*variable) error {
+	var errs []string
+	for _, v := range variables {
+		if v.existing == nil {
+			err := s.varSVC.DeleteVariable(context.Background(), v.ID())
+			if err != nil {
+				errs = append(errs, v.ID().String())
+			}
+			continue
+		}
+
+		_, err := s.varSVC.UpdateVariable(context.Background(), v.ID(), &influxdb.VariableUpdate{
+			Description: v.existing.Description,
+			Arguments:   v.existing.Arguments,
+		})
+		if err != nil {
+			errs = append(errs, v.ID().String())
+		}
+	}
+
+	if len(errs) > 0 {
+		return fmt.Errorf(`variable_ids=[%s] err="unable to delete variable"`, strings.Join(errs, ", "))
+	}
+
+	return nil
+}
+
+func (s *Service) applyVariable(ctx context.Context, v *variable) (influxdb.Variable, error) {
+	if v.existing != nil {
+		updatedVar, err := s.varSVC.UpdateVariable(ctx, v.ID(), &influxdb.VariableUpdate{
+			Description: v.Description,
+			Arguments:   v.influxVarArgs(),
+		})
+		if err != nil {
+			return influxdb.Variable{}, err
+		}
+		return *updatedVar, nil
+	}
+
+	influxVar := influxdb.Variable{
+		OrganizationID: v.OrgID,
+		Name:           v.Name,
+		Description:    v.Description,
+		Arguments:      v.influxVarArgs(),
+	}
+	err := s.varSVC.CreateVariable(ctx, &influxVar)
+	if err != nil {
+		return influxdb.Variable{}, err
+	}
+
+	return influxVar, nil
+}
+
 func (s *Service) applyLabelMappings(pkg *Pkg) applier {
 	var mappings []influxdb.LabelMapping
 	createFn := func(ctx context.Context, orgID influxdb.ID) error {
diff --git a/pkger/service_test.go b/pkger/service_test.go
index 316a336e84..cddfc2f90c 100644
--- a/pkger/service_test.go
+++ b/pkger/service_test.go
@@ -10,7 +10,6 @@ import (
 	"github.com/influxdata/influxdb/mock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"go.uber.org/zap"
 )
 
 func TestService(t *testing.T) {
@@ -28,9 +27,7 @@ func TestService(t *testing.T) {
 							RetentionPeriod: 30 * time.Hour,
 						}, nil
 					}
-					fakeLabelSVC := mock.NewLabelService()
-					fakeDashSVC := mock.NewDashboardService()
-					svc := NewService(zap.NewNop(), fakeBktSVC, fakeLabelSVC, fakeDashSVC)
+					svc := NewService(WithBucketSVC(fakeBktSVC), WithLabelSVC(mock.NewLabelService()))
 
 					_, diff, err := svc.DryRun(context.TODO(), influxdb.ID(100), pkg)
 					require.NoError(t, err)
@@ -55,9 +52,7 @@ func TestService(t *testing.T) {
 					fakeBktSVC.FindBucketByNameFn = func(_ context.Context, orgID influxdb.ID, name string) (*influxdb.Bucket, error) {
 						return nil, errors.New("not found")
 					}
-					fakeLabelSVC := mock.NewLabelService()
-					fakeDashSVC := mock.NewDashboardService()
-					svc := NewService(zap.NewNop(), fakeBktSVC, fakeLabelSVC, fakeDashSVC)
+					svc := NewService(WithBucketSVC(fakeBktSVC), WithLabelSVC(mock.NewLabelService()))
 
 					_, diff, err := svc.DryRun(context.TODO(), influxdb.ID(100), pkg)
 					require.NoError(t, err)
@@ -77,7 +72,6 @@ func TestService(t *testing.T) {
 		t.Run("labels", func(t *testing.T) {
 			t.Run("two labels updated", func(t *testing.T) {
 				testfileRunner(t, "testdata/label", func(t *testing.T, pkg *Pkg) {
-					fakeBktSVC := mock.NewBucketService()
 					fakeLabelSVC := mock.NewLabelService()
 					fakeLabelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
 						return []*influxdb.Label{
@@ -91,8 +85,7 @@ func TestService(t *testing.T) {
 							},
 						}, nil
 					}
-					fakeDashSVC := mock.NewDashboardService()
-					svc := NewService(zap.NewNop(), fakeBktSVC, fakeLabelSVC, fakeDashSVC)
+					svc := NewService(WithLabelSVC(fakeLabelSVC))
 
 					_, diff, err := svc.DryRun(context.TODO(), influxdb.ID(100), pkg)
 					require.NoError(t, err)
@@ -118,13 +111,11 @@ func TestService(t *testing.T) {
 
 			t.Run("two labels created", func(t *testing.T) {
 				testfileRunner(t, "testdata/label", func(t *testing.T, pkg *Pkg) {
-					fakeBktSVC := mock.NewBucketService()
 					fakeLabelSVC := mock.NewLabelService()
 					fakeLabelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
 						return nil, errors.New("no labels found")
 					}
-					fakeDashSVC := mock.NewDashboardService()
-					svc := NewService(zap.NewNop(), fakeBktSVC, fakeLabelSVC, fakeDashSVC)
+					svc := NewService(WithLabelSVC(fakeLabelSVC))
 
 					_, diff, err := svc.DryRun(context.TODO(), influxdb.ID(100), pkg)
 					require.NoError(t, err)
@@ -145,26 +136,75 @@ func TestService(t *testing.T) {
 				})
 			})
 		})
+
+		t.Run("variables", func(t *testing.T) {
+			testfileRunner(t, "testdata/variables", func(t *testing.T, pkg *Pkg) {
+				fakeVarSVC := mock.NewVariableService()
+				fakeVarSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, opts ...influxdb.FindOptions) ([]*influxdb.Variable, error) {
+					return []*influxdb.Variable{
+						{
+							ID:          influxdb.ID(1),
+							Name:        "var_const",
+							Description: "old desc",
+						},
+					}, nil
+				}
+				fakeLabelSVC := mock.NewLabelService() // ignore mappings for now
+				svc := NewService(
+					WithLabelSVC(fakeLabelSVC),
+					WithVariableSVC(fakeVarSVC),
+				)
+
+				_, diff, err := svc.DryRun(context.TODO(), influxdb.ID(100), pkg)
+				require.NoError(t, err)
+
+				require.Len(t, diff.Variables, 4)
+
+				expected := DiffVariable{
+					ID:      SafeID(1),
+					Name:    "var_const",
+					OldDesc: "old desc",
+					NewDesc: "var_const desc",
+					NewArgs: &influxdb.VariableArguments{
+						Type:   "constant",
+						Values: influxdb.VariableConstantValues{"first val"},
+					},
+				}
+				assert.Equal(t, expected, diff.Variables[0])
+
+				expected = DiffVariable{
+					// no ID here since this one would be new
+					Name:    "var_map",
+					OldDesc: "",
+					NewDesc: "var_map desc",
+					NewArgs: &influxdb.VariableArguments{
+						Type:   "map",
+						Values: influxdb.VariableMapValues{"k1": "v1"},
+					},
+				}
+				assert.Equal(t, expected, diff.Variables[1])
+			})
+		})
 	})
 
 	t.Run("Apply", func(t *testing.T) {
 		t.Run("buckets", func(t *testing.T) {
 			t.Run("successfully creates pkg of buckets", func(t *testing.T) {
-				testfileRunner(t, "testdata/bucket", func(t *testing.T, pkg *Pkg) {
-					fakeBucketSVC := mock.NewBucketService()
-					fakeBucketSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
+				testfileRunner(t, "testdata/bucket.yml", func(t *testing.T, pkg *Pkg) {
+					fakeBktSVC := mock.NewBucketService()
+					fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
 						b.ID = influxdb.ID(b.RetentionPeriod)
 						return nil
 					}
-					fakeBucketSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.ID, s string) (*influxdb.Bucket, error) {
+					fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.ID, s string) (*influxdb.Bucket, error) {
 						// forces the bucket to be created a new
 						return nil, errors.New("an error")
 					}
-					fakeBucketSVC.UpdateBucketFn = func(_ context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
+					fakeBktSVC.UpdateBucketFn = func(_ context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
 						return &influxdb.Bucket{ID: id}, nil
 					}
 
-					svc := NewService(zap.NewNop(), fakeBucketSVC, nil, nil)
+					svc := NewService(WithBucketSVC(fakeBktSVC))
 
 					orgID := influxdb.ID(9000)
 
@@ -196,19 +236,19 @@ func TestService(t *testing.T) {
 						RetentionPeriod: pkgBkt.RetentionPeriod,
 					}
 
-					fakeBucketSVC := mock.NewBucketService()
+					fakeBktSVC := mock.NewBucketService()
 					var createCallCount int
-					fakeBucketSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
+					fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
 						createCallCount++
 						return nil
 					}
 					var updateCallCount int
-					fakeBucketSVC.UpdateBucketFn = func(_ context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
+					fakeBktSVC.UpdateBucketFn = func(_ context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
 						updateCallCount++
 						return &influxdb.Bucket{ID: id}, nil
 					}
 
-					svc := NewService(zap.NewNop(), fakeBucketSVC, nil, nil)
+					svc := NewService(WithBucketSVC(fakeBktSVC))
 
 					sum, err := svc.Apply(context.TODO(), orgID, pkg)
 					require.NoError(t, err)
@@ -227,13 +267,13 @@ func TestService(t *testing.T) {
 
 			t.Run("rolls back all created buckets on an error", func(t *testing.T) {
 				testfileRunner(t, "testdata/bucket", func(t *testing.T, pkg *Pkg) {
-					fakeBucketSVC := mock.NewBucketService()
-					fakeBucketSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.ID, s string) (*influxdb.Bucket, error) {
+					fakeBktSVC := mock.NewBucketService()
+					fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.ID, s string) (*influxdb.Bucket, error) {
 						// forces the bucket to be created a new
 						return nil, errors.New("an error")
 					}
 					var c int
-					fakeBucketSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
+					fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
 						if c == 2 {
 							return errors.New("blowed up ")
 						}
@@ -241,7 +281,7 @@ func TestService(t *testing.T) {
 						return nil
 					}
 					var count int
-					fakeBucketSVC.DeleteBucketFn = func(_ context.Context, id influxdb.ID) error {
+					fakeBktSVC.DeleteBucketFn = func(_ context.Context, id influxdb.ID) error {
 						count++
 						return nil
 					}
@@ -249,7 +289,7 @@ func TestService(t *testing.T) {
 					pkg.mBuckets["copybuck1"] = pkg.mBuckets["rucket_11"]
 					pkg.mBuckets["copybuck2"] = pkg.mBuckets["rucket_11"]
 
-					svc := NewService(zap.NewNop(), fakeBucketSVC, nil, nil)
+					svc := NewService(WithBucketSVC(fakeBktSVC))
 
 					orgID := influxdb.ID(9000)
 
@@ -272,7 +312,7 @@ func TestService(t *testing.T) {
 						return nil
 					}
 
-					svc := NewService(zap.NewNop(), nil, fakeLabelSVC, nil)
+					svc := NewService(WithLabelSVC(fakeLabelSVC))
 
 					orgID := influxdb.ID(9000)
 
@@ -313,12 +353,11 @@ func TestService(t *testing.T) {
 						count++
 						return nil
 					}
-					fakeDashSVC := mock.NewDashboardService()
 
 					pkg.mLabels["copy1"] = pkg.mLabels["label_1"]
 					pkg.mLabels["copy2"] = pkg.mLabels["label_2"]
 
-					svc := NewService(zap.NewNop(), nil, fakeLabelSVC, fakeDashSVC)
+					svc := NewService(WithLabelSVC(fakeLabelSVC))
 
 					orgID := influxdb.ID(9000)
 
@@ -363,7 +402,7 @@ func TestService(t *testing.T) {
 						return &influxdb.Label{ID: id}, nil
 					}
 
-					svc := NewService(zap.NewNop(), nil, fakeLabelSVC, nil)
+					svc := NewService(WithLabelSVC(fakeLabelSVC))
 
 					sum, err := svc.Apply(context.TODO(), orgID, pkg)
 					require.NoError(t, err)
@@ -386,12 +425,11 @@ func TestService(t *testing.T) {
 					assert.Equal(t, 1, createCallCount) // only called for second label
 				})
 			})
-
 		})
 
 		t.Run("dashboards", func(t *testing.T) {
 			t.Run("successfully creates a dashboard", func(t *testing.T) {
-				testfileRunner(t, "testdata/dashboard", func(t *testing.T, pkg *Pkg) {
+				testfileRunner(t, "testdata/dashboard.yml", func(t *testing.T, pkg *Pkg) {
 					fakeDashSVC := mock.NewDashboardService()
 					id := 1
 					fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error {
@@ -405,7 +443,7 @@ func TestService(t *testing.T) {
 						return &influxdb.View{}, nil
 					}
 
-					svc := NewService(zap.NewNop(), nil, nil, fakeDashSVC)
+					svc := NewService(WithDashboardSVC(fakeDashSVC))
 
 					orgID := influxdb.ID(9000)
 
@@ -422,7 +460,7 @@ func TestService(t *testing.T) {
 			})
 
 			t.Run("rolls back created dashboard on an error", func(t *testing.T) {
-				testfileRunner(t, "testdata/dashboard", func(t *testing.T, pkg *Pkg) {
+				testfileRunner(t, "testdata/dashboard.yml", func(t *testing.T, pkg *Pkg) {
 					fakeDashSVC := mock.NewDashboardService()
 					var c int
 					fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error {
@@ -442,7 +480,7 @@ func TestService(t *testing.T) {
 
 					pkg.mDashboards["copy1"] = pkg.mDashboards["dash_1"]
 
-					svc := NewService(zap.NewNop(), nil, nil, fakeDashSVC)
+					svc := NewService(WithDashboardSVC(fakeDashSVC))
 
 					orgID := influxdb.ID(9000)
 
@@ -456,7 +494,7 @@ func TestService(t *testing.T) {
 
 		t.Run("label mapping", func(t *testing.T) {
 			t.Run("successfully creates pkg of labels", func(t *testing.T) {
-				testfileRunner(t, "testdata/bucket_associates_label", func(t *testing.T, pkg *Pkg) {
+				testfileRunner(t, "testdata/bucket_associates_label.yml", func(t *testing.T, pkg *Pkg) {
 					fakeBktSVC := mock.NewBucketService()
 					id := 1
 					fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error {
@@ -482,7 +520,11 @@ func TestService(t *testing.T) {
 						return nil
 					}
 					fakeDashSVC := mock.NewDashboardService()
-					svc := NewService(zap.NewNop(), fakeBktSVC, fakeLabelSVC, fakeDashSVC)
+					svc := NewService(
+						WithBucketSVC(fakeBktSVC),
+						WithLabelSVC(fakeLabelSVC),
+						WithDashboardSVC(fakeDashSVC),
+					)
 
 					orgID := influxdb.ID(9000)
 
@@ -493,6 +535,126 @@ func TestService(t *testing.T) {
 				})
 			})
 		})
+
+		t.Run("variables", func(t *testing.T) {
+			t.Run("successfully creates pkg of variables", func(t *testing.T) {
+				testfileRunner(t, "testdata/variables.yml", func(t *testing.T, pkg *Pkg) {
+					fakeVarSVC := mock.NewVariableService()
+					id := 1
+					fakeVarSVC.CreateVariableF = func(_ context.Context, v *influxdb.Variable) error {
+						v.ID = influxdb.ID(id)
+						id++
+						return nil
+					}
+
+					svc := NewService(
+						WithLabelSVC(mock.NewLabelService()),
+						WithVariableSVC(fakeVarSVC),
+					)
+
+					orgID := influxdb.ID(9000)
+
+					sum, err := svc.Apply(context.TODO(), orgID, pkg)
+					require.NoError(t, err)
+
+					require.Len(t, sum.Variables, 4)
+					expected := sum.Variables[0]
+					assert.Equal(t, influxdb.ID(1), expected.ID)
+					assert.Equal(t, orgID, expected.OrganizationID)
+					assert.Equal(t, "var_const", expected.Name)
+					assert.Equal(t, "var_const desc", expected.Description)
+					require.NotNil(t, expected.Arguments)
+					assert.Equal(t, influxdb.VariableConstantValues{"first val"}, expected.Arguments.Values)
+
+					for i := 1; i < 3; i++ {
+						expected = sum.Variables[i]
+						assert.Equal(t, influxdb.ID(i+1), expected.ID)
+					}
+				})
+			})
+
+			t.Run("rolls back all created variables on an error", func(t *testing.T) {
+				testfileRunner(t, "testdata/variables.yml", func(t *testing.T, pkg *Pkg) {
+					fakeVarSVC := mock.NewVariableService()
+					var c int
+					fakeVarSVC.CreateVariableF = func(_ context.Context, l *influxdb.Variable) error {
+						// 4th variable will return the error here, and 3 before should be rolled back
+						if c == 3 {
+							return errors.New("blowed up ")
+						}
+						c++
+						return nil
+					}
+					var count int
+					fakeVarSVC.DeleteVariableF = func(_ context.Context, id influxdb.ID) error {
+						count++
+						return nil
+					}
+
+					svc := NewService(
+						WithLabelSVC(mock.NewLabelService()),
+						WithVariableSVC(fakeVarSVC),
+					)
+
+					orgID := influxdb.ID(9000)
+
+					_, err := svc.Apply(context.TODO(), orgID, pkg)
+					require.Error(t, err)
+
+					assert.Equal(t, 3, count)
+				})
+			})
+
+			t.Run("will not apply variable if no changes to be applied", func(t *testing.T) {
+				testfileRunner(t, "testdata/variables.yml", func(t *testing.T, pkg *Pkg) {
+					orgID := influxdb.ID(9000)
+
+					pkg.isVerified = true
+					pkgLabel := pkg.mVariables["var_const"]
+					pkgLabel.existing = &influxdb.Variable{
+						// makes all pkg changes same as they are on the existing
+						ID:             influxdb.ID(1),
+						OrganizationID: orgID,
+						Name:           pkgLabel.Name,
+						Arguments: &influxdb.VariableArguments{
+							Type:   "constant",
+							Values: influxdb.VariableConstantValues{"first val"},
+						},
+					}
+
+					fakeVarSVC := mock.NewVariableService()
+					var createCallCount int
+					fakeVarSVC.CreateVariableF = func(_ context.Context, l *influxdb.Variable) error {
+						createCallCount++
+						if l.Name == "var_const" {
+							return errors.New("shouldn't get here")
+						}
+						return nil
+					}
+					fakeVarSVC.UpdateVariableF = func(_ context.Context, id influxdb.ID, v *influxdb.VariableUpdate) (*influxdb.Variable, error) {
+						if id > influxdb.ID(1) {
+							return nil, errors.New("this id should not be updated")
+						}
+						return &influxdb.Variable{ID: id}, nil
+					}
+
+					svc := NewService(
+						WithLabelSVC(mock.NewLabelService()),
+						WithVariableSVC(fakeVarSVC),
+					)
+
+					sum, err := svc.Apply(context.TODO(), orgID, pkg)
+					require.NoError(t, err)
+
+					require.Len(t, sum.Variables, 4)
+					expected := sum.Variables[0]
+					assert.Equal(t, influxdb.ID(1), expected.ID)
+					assert.Equal(t, "var_const", expected.Name)
+
+					assert.Equal(t, 3, createCallCount) // only called for last 3 labels
+				})
+			})
+		})
 	})
 
 	t.Run("CreatePkg", func(t *testing.T) {
diff --git a/pkger/testdata/variables_associates_label.yml b/pkger/testdata/variables_associates_label.yml
new file mode 100644
index 0000000000..e1f822175a
--- /dev/null
+++ b/pkger/testdata/variables_associates_label.yml
@@ -0,0 +1,18 @@
+apiVersion: 0.1.0
+kind: Package
+meta:
+  pkgName:      pkg_name
+  pkgVersion:   1
+  description:  pack description
+spec:
+  resources:
+    - kind: Label
+      name: label_1
+    - kind: Variable
+      name: var_1
+      type: constant
+      values:
+        - first val
+      associations:
+        - kind: Label
+          name: label_1