diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go
index 21df43408755d1d9b689e15c490c9cf9fcf4102c..8857bf441ea68c89846fa11dc169ae05cff1d10e 100644
--- a/pkg/chart/chart.go
+++ b/pkg/chart/chart.go
@@ -20,6 +20,7 @@ import (
 	"archive/tar"
 	"bytes"
 	"compress/gzip"
+	"encoding/base64"
 	"errors"
 	"fmt"
 	"io"
@@ -243,25 +244,7 @@ func LoadDir(chart string) (*Chart, error) {
 
 // LoadData loads a chart from data, where data is a []byte containing a gzipped tar file.
 func LoadData(data []byte) (*Chart, error) {
-	b := bytes.NewBuffer(data)
-	unzipped, err := gzip.NewReader(b)
-	if err != nil {
-		return nil, err
-	}
-	defer unzipped.Close()
-
-	untarred := tar.NewReader(unzipped)
-	c, err := loadTar(untarred)
-	if err != nil {
-		return nil, err
-	}
-
-	cf, err := LoadChartfile(filepath.Join(c.tmpDir, ChartfileName))
-	if err != nil {
-		return nil, err
-	}
-	c.chartyaml = cf
-	return &Chart{loader: c}, nil
+	return LoadDataFromReader(bytes.NewBuffer(data))
 }
 
 // Load loads a chart from a chart archive.
@@ -281,7 +264,11 @@ func Load(archive string) (*Chart, error) {
 	}
 	defer raw.Close()
 
-	unzipped, err := gzip.NewReader(raw)
+	return LoadDataFromReader(raw)
+}
+
+func LoadDataFromReader(r io.Reader) (*Chart, error) {
+	unzipped, err := gzip.NewReader(r)
 	if err != nil {
 		return nil, err
 	}
@@ -367,3 +354,65 @@ func loadTar(r *tar.Reader) (*tarChart, error) {
 
 	return c, nil
 }
+
+// ChartMember is a file in a chart.
+type ChartMember struct {
+	Path    string `json:"path"`    // Path from the root of the chart.
+	Content []byte `json:"content"` // Base64 encoded content.
+}
+
+// LoadTemplates loads the members of TemplatesDir().
+func (c *Chart) LoadTemplates() ([]*ChartMember, error) {
+	dir := c.TemplatesDir()
+	return c.loadDirectory(dir)
+}
+
+// loadDirectory loads the members of a directory.
+func (c *Chart) loadDirectory(dir string) ([]*ChartMember, error) {
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return nil, err
+	}
+
+	members := []*ChartMember{}
+	for _, file := range files {
+		filename := filepath.Join(dir, file.Name())
+		member, err := c.loadMember(filename)
+		if err != nil {
+			return nil, err
+		}
+
+		members = append(members, member)
+	}
+
+	return members, nil
+}
+
+// path is from the root of the chart.
+func (c *Chart) LoadMember(path string) (*ChartMember, error) {
+	filename := filepath.Join(c.loader.dir(), path)
+	return c.loadMember(filename)
+}
+
+// loadMember loads and base 64 encodes a file.
+func (c *Chart) loadMember(filename string) (*ChartMember, error) {
+	dir := c.Dir()
+	if !strings.HasPrefix(filename, dir) {
+		err := fmt.Errorf("File %s is outside chart directory %s", filename, dir)
+		return nil, err
+	}
+
+	b, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	path := strings.TrimPrefix(filename, dir)
+	content := base64.StdEncoding.EncodeToString(b)
+	result := &ChartMember{
+		Path:    path,
+		Content: []byte(content),
+	}
+
+	return result, nil
+}
diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go
index 36ac856d20df1fe89fde3059e9d9968940140e9b..5d1c46f6706edbc6e9fe9c08ec51e95fb027bdb6 100644
--- a/pkg/chart/chart_test.go
+++ b/pkg/chart/chart_test.go
@@ -17,6 +17,8 @@ limitations under the License.
 package chart
 
 import (
+	"encoding/base64"
+	"fmt"
 	"io/ioutil"
 	"path/filepath"
 	"testing"
@@ -30,6 +32,7 @@ const (
 	testarchive = "testdata/frobnitz-0.0.1.tgz"
 	testill     = "testdata/ill-1.2.3.tgz"
 	testnochart = "testdata/nochart.tgz"
+	testmember  = "templates/wordpress.jinja"
 )
 
 // Type canaries. If these fail, they will fail at compile time.
@@ -160,3 +163,91 @@ func TestChart(t *testing.T) {
 		t.Errorf("Unexpectedly, icon is in %s", i)
 	}
 }
+
+func TestLoadTemplates(t *testing.T) {
+	c, err := LoadDir(testdir)
+	if err != nil {
+		t.Errorf("Failed to load chart: %s", err)
+	}
+
+	members, err := c.LoadTemplates()
+	if members == nil {
+		t.Fatalf("Cannot load templates: unknown error")
+	}
+
+	if err != nil {
+		t.Fatalf("Cannot load templates: %s", err)
+	}
+
+	dir := c.TemplatesDir()
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		t.Fatalf("Cannot read template directory: %s", err)
+	}
+
+	if len(members) != len(files) {
+		t.Fatalf("Expected %s templates, got %d", len(files), len(members))
+	}
+
+	root := c.loader.dir()
+	for _, file := range files {
+		path := filepath.Join(preTemplates, file.Name())
+		if err := findMember(root, path, members); err != nil {
+			t.Fatal(err)
+		}
+	}
+}
+
+func findMember(root, path string, members []*ChartMember) error {
+	for _, member := range members {
+		if member.Path == path {
+			filename := filepath.Join(root, path)
+			if err := compareContent(filename, string(member.Content)); err != nil {
+				return err
+			}
+
+			return nil
+		}
+	}
+
+	return fmt.Errorf("Template not found: %s", path)
+}
+
+func TestLoadMember(t *testing.T) {
+	c, err := LoadDir(testdir)
+	if err != nil {
+		t.Errorf("Failed to load chart: %s", err)
+	}
+
+	member, err := c.LoadMember(testmember)
+	if member == nil {
+		t.Fatalf("Cannot load member %s: unknown error", testmember)
+	}
+
+	if err != nil {
+		t.Fatalf("Cannot load member %s: %s", testmember, err)
+	}
+
+	if member.Path != testmember {
+		t.Errorf("Expected member path %s, got %s", testmember, member.Path)
+	}
+
+	filename := filepath.Join(c.loader.dir(), testmember)
+	if err := compareContent(filename, string(member.Content)); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func compareContent(filename, content string) error {
+	b, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return fmt.Errorf("Cannot read test file %s: %s", filename, err)
+	}
+
+	compare := base64.StdEncoding.EncodeToString(b)
+	if content != compare {
+		return fmt.Errorf("Expected member content\n%v\ngot\n%v", []byte(compare), []byte(content))
+	}
+
+	return nil
+}
diff --git a/pkg/chart/chartfile.go b/pkg/chart/chartfile.go
index b694a373465d5a57eb257c3008f2f6016a885ab7..1332ef805ba6d3bd2c1fb4056c42a7cd0d5f3126 100644
--- a/pkg/chart/chartfile.go
+++ b/pkg/chart/chartfile.go
@@ -34,6 +34,8 @@ type Chartfile struct {
 	Home         string           `yaml:"home"`
 	Dependencies []*Dependency    `yaml:"dependencies,omitempty"`
 	Environment  []*EnvConstraint `yaml:"environment,omitempty"`
+	Expander     *Expander        `yaml:"expander,omitempty"`
+	Schema       string           `yaml:"schema,omitempty"`
 }
 
 // Maintainer describes a chart maintainer.
@@ -57,6 +59,14 @@ type EnvConstraint struct {
 	APIGroups  []string `yaml:"apiGroups,omitempty"`
 }
 
+// Expander controls how template/ is evaluated.
+type Expander struct {
+	// Currently just Expandybird or GoTemplate
+	Name string `json:"name"`
+	// During evaluation, which file to start from.
+	Entrypoint string `json:"entrypoint"`
+}
+
 // LoadChartfile loads a Chart.yaml file into a *Chart.
 func LoadChartfile(filename string) (*Chartfile, error) {
 	b, err := ioutil.ReadFile(filename)
diff --git a/pkg/chart/chartfile_test.go b/pkg/chart/chartfile_test.go
index b5e46dfa6f345bd6200106f9690052abc42ef87f..3c4042db7a4bca7f207bc5c2858fdb016ff25a3b 100644
--- a/pkg/chart/chartfile_test.go
+++ b/pkg/chart/chartfile_test.go
@@ -50,6 +50,23 @@ func TestLoadChartfile(t *testing.T) {
 	if f.Source[0] != "https://example.com/foo/bar" {
 		t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source)
 	}
+
+	expander := f.Expander
+	if expander == nil {
+		t.Errorf("No expander found in %s", testfile)
+	} else {
+		if expander.Name != "Expandybird" {
+			t.Errorf("Expected expander name Expandybird, got %s", expander.Name)
+		}
+
+		if expander.Entrypoint != "templates/wordpress.jinja" {
+			t.Errorf("Expected expander entrypoint templates/wordpress.jinja, got %s", expander.Entrypoint)
+		}
+	}
+
+	if f.Schema != "wordpress.jinja.schema" {
+		t.Errorf("Expected schema wordpress.jinja.schema, got %s", f.Schema)
+	}
 }
 
 func TestVersionOK(t *testing.T) {
diff --git a/pkg/chart/testdata/frobnitz/Chart.yaml b/pkg/chart/testdata/frobnitz/Chart.yaml
index 9572f010c1826cadced546702a14627e0c0fb6d9..b1e67a038f8cfca9bf6158d35e85db44068ddae4 100644
--- a/pkg/chart/testdata/frobnitz/Chart.yaml
+++ b/pkg/chart/testdata/frobnitz/Chart.yaml
@@ -26,3 +26,8 @@ environment:
       - extensions/v1beta1/daemonset
     apiGroups:
       - 3rdParty
+expander:
+  name: Expandybird
+  entrypoint: templates/wordpress.jinja
+schema: wordpress.jinja.schema
+  
\ No newline at end of file
diff --git a/pkg/common/types.go b/pkg/common/types.go
index b7dbe6fc03948bba2ad765b164836984055c69de..df030da8773f290461a5e5eb5d77012da2a16814 100644
--- a/pkg/common/types.go
+++ b/pkg/common/types.go
@@ -97,28 +97,6 @@ type Manifest struct {
 	Layout         *Layout        `json:"layout,omitempty"`
 }
 
-// Expander controls how template/ is evaluated.
-type Expander struct {
-	// Currently just Expandybird or GoTemplate
-	Name string `json:"name"`
-	// During evaluation, which file to start from.
-	Entrypoint string `json:"entry_point"`
-}
-
-// ChartFile is a file in a chart that is not chart.yaml.
-type ChartFile struct {
-	Path    string `json:"path"`    // Path from the root of the chart.
-	Content string `json:"content"` // Base64 encoded file content.
-}
-
-// Chart is our internal representation of the chart.yaml (in structured form) + all supporting files.
-type Chart struct {
-	Name     string       `json:"name"`
-	Expander *Expander    `json:"expander"`
-	Schema   interface{}  `json:"schema"`
-	Files    []*ChartFile `json:"files"`
-}
-
 // Template describes a set of resources to be deployed.
 // Manager expands a Template into a Configuration, which
 // describes the set in a form that can be instantiated.
@@ -227,10 +205,10 @@ type Registry struct {
 	CredentialName string         `json:"credentialname,omitempty"` // Name of the credential to use
 }
 
-// RegistryType defines the technology that implements the registry
+// RegistryType defines the technology that implements a registry.
 type RegistryType string
 
-// Constants that identify the supported registry layouts.
+// Constants that identify the supported registry types.
 const (
 	GithubRegistryType RegistryType = "github"
 	GCSRegistryType    RegistryType = "gcs"
@@ -256,6 +234,34 @@ const (
 	OneLevelRegistry RegistryFormat = "onelevel"
 )
 
+// RepoType defines the technology that implements a repository.
+type RepoType string
+
+// Constants that identify the supported repository types.
+const (
+	GCSRepoType RepoType = "gcs"
+)
+
+// RepoFormat is a semi-colon delimited string that describes the format
+// of a repository.
+type RepoFormat string
+
+const (
+	// Versioning.
+
+	// VersionedRepo identifies a versioned repository, where types appear under versions.
+	VersionedRepo RepoFormat = "versioned"
+	// UnversionedRepo identifies an unversioned repository, where types appear under their names.
+	UnversionedRepo RepoFormat = "unversioned"
+
+	// Organization.
+
+	// CollectionRepo identfies a collection repository, where types are grouped into collections.
+	CollectionRepo RepoFormat = "collection"
+	// OneLevelRepo identifies a one level repository, where all types appear at the top level.
+	OneLevelRepo RepoFormat = "onelevel"
+)
+
 // RegistryService maintains a set of registries that defines the scope of all
 // registry based operations, such as search and type resolution.
 type RegistryService interface {
diff --git a/pkg/repo/gcs_repo.go b/pkg/repo/gcs_repo.go
new file mode 100644
index 0000000000000000000000000000000000000000..0feb7793461087c94c95a61ae15c88624e69d754
--- /dev/null
+++ b/pkg/repo/gcs_repo.go
@@ -0,0 +1,162 @@
+/*
+Copyright 2015 The Kubernetes Authors All rights reserved.
+
+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 repo
+
+import (
+	"github.com/kubernetes/helm/pkg/chart"
+	"github.com/kubernetes/helm/pkg/common"
+	"github.com/kubernetes/helm/pkg/util"
+
+	storage "google.golang.org/api/storage/v1"
+
+	"fmt"
+	"net/http"
+	"net/url"
+	"regexp"
+)
+
+// GCSRepo implements the ObjectStorageRepo interface
+// for Google Cloud Storage.
+//
+// A GCSRepo root must be a directory that contains all the available charts.
+type GCSRepo struct {
+	chartRepo      // A GCSRepo is a chartRepo
+	bucket         string
+	credentialName string
+	httpClient     *http.Client
+	service        *storage.Service
+}
+
+// URLFormatMatcher matches the GCS URL format (gs:).
+var URLFormatMatcher = regexp.MustCompile("gs://(.*)")
+
+var GCSRepoFormat = common.RepoFormat(fmt.Sprintf("%s;%s", common.UnversionedRepo, common.OneLevelRepo))
+
+// NewGCSRepo creates a GCS repository.
+func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) {
+	m := URLFormatMatcher.FindStringSubmatch(URL)
+	if len(m) != 2 {
+		return nil, fmt.Errorf("URL must be of the form gs://<bucket>, was %s", URL)
+	}
+
+	cr, err := newRepo(name, URL, string(GCSRepoFormat), string(common.GCSRepoType))
+	if err != nil {
+		return nil, err
+	}
+
+	if httpClient == nil {
+		httpClient = http.DefaultClient
+	}
+
+	gs, err := storage.New(httpClient)
+	if err != nil {
+		return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err)
+	}
+
+	result := &GCSRepo{
+		chartRepo:  *cr,
+		httpClient: httpClient,
+		service:    gs,
+		bucket:     m[1],
+	}
+
+	return result, nil
+}
+
+// GetBucket returns the repository bucket.
+func (g *GCSRepo) GetBucket() string {
+	return g.bucket
+}
+
+// ListCharts lists charts in this chart repository whose string values conform to the
+// supplied regular expression, or all charts, if the regular expression is nil.
+func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
+	// List all files in the bucket/prefix that contain the
+	charts := []string{}
+
+	// List all objects in a bucket using pagination
+	pageToken := ""
+	for {
+		call := g.service.Objects.List(g.bucket)
+		call.Delimiter("/")
+		if pageToken != "" {
+			call = call.PageToken(pageToken)
+		}
+		res, err := call.Do()
+		if err != nil {
+			return nil, err
+		}
+		for _, object := range res.Items {
+			// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here
+			m := ChartNameMatcher.FindStringSubmatch(object.Name)
+			if len(m) != 3 {
+				continue
+			}
+
+			if regex == nil || regex.MatchString(object.Name) {
+				charts = append(charts, object.Name)
+			}
+		}
+
+		if pageToken = res.NextPageToken; pageToken == "" {
+			break
+		}
+	}
+
+	return charts, nil
+}
+
+// GetChart retrieves, unpacks and returns a chart by name.
+func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) {
+	// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here
+	if !ChartNameMatcher.MatchString(name) {
+		return nil, fmt.Errorf("name must be of the form <name>-<version>.tgz, was %s", name)
+	}
+
+	call := g.service.Objects.Get(g.bucket, name)
+	object, err := call.Do()
+	if err != nil {
+		return nil, err
+	}
+
+	u, err := url.Parse(object.MediaLink)
+	if err != nil {
+		return nil, fmt.Errorf("Cannot parse URL %s for chart %s/%s: %s",
+			object.MediaLink, object.Bucket, object.Name, err)
+	}
+
+	getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper())
+	body, code, err := getter.Get(u.String())
+	if err != nil {
+		return nil, fmt.Errorf("Cannot fetch URL %s for chart %s/%s: %d %s",
+			object.MediaLink, object.Bucket, object.Name, code, err)
+	}
+
+	return chart.Load(body)
+}
+
+// Do performs an HTTP operation on the receiver's httpClient.
+func (g *GCSRepo) Do(req *http.Request) (resp *http.Response, err error) {
+	return g.httpClient.Do(req)
+}
+
+// TODO: Remove GetShortURL when no longer needed.
+
+// GetShortURL returns the URL without the scheme.
+func (g GCSRepo) GetShortURL() string {
+	return util.TrimURLScheme(g.URL)
+}
diff --git a/pkg/repo/gcs_repo_test.go b/pkg/repo/gcs_repo_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..52cf5863e2c86d40d97de9a573ed327d4f7085aa
--- /dev/null
+++ b/pkg/repo/gcs_repo_test.go
@@ -0,0 +1,155 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+
+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 repo
+
+import (
+	"github.com/kubernetes/helm/pkg/chart"
+	"github.com/kubernetes/helm/pkg/common"
+
+	"os"
+	"reflect"
+	"regexp"
+	"testing"
+)
+
+var (
+	TestArchiveBucket      = os.Getenv("TEST_ARCHIVE_BUCKET")
+	TestArchiveName        = "frobnitz-0.0.1.tgz"
+	TestChartFile          = "testdata/frobnitz/Chart.yaml"
+	TestShouldFindRegex    = regexp.MustCompile(TestArchiveName)
+	TestShouldNotFindRegex = regexp.MustCompile("foobar")
+)
+
+func TestValidGSURL(t *testing.T) {
+	var validURL = "gs://bucket"
+	tr, err := NewGCSRepo("testName", validURL, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	wantType := common.GCSRepoType
+	haveType := tr.GetRepoType()
+	if haveType != wantType {
+		t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType)
+	}
+
+	wantFormat := GCSRepoFormat
+	haveFormat := tr.GetRepoFormat()
+	if haveFormat != wantFormat {
+		t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
+	}
+
+}
+
+func TestInvalidGSURL(t *testing.T) {
+	var invalidURL = "https://bucket"
+	_, err := NewGCSRepo("testName", invalidURL, nil)
+	if err == nil {
+		t.Fatalf("expected error did not occur for invalid URL")
+	}
+}
+
+func TestListCharts(t *testing.T) {
+	if TestArchiveBucket != "" {
+		tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		charts, err := tr.ListCharts(nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(charts) != 1 {
+			t.Fatalf("expected one chart in test bucket, got %d", len(charts))
+		}
+
+		name := charts[0]
+		if name != TestArchiveName {
+			t.Fatalf("expected chart named %s in test bucket, got %s", TestArchiveName, name)
+		}
+	}
+}
+
+func TestListChartsWithShouldFindRegex(t *testing.T) {
+	if TestArchiveBucket != "" {
+		tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		charts, err := tr.ListCharts(TestShouldFindRegex)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(charts) != 1 {
+			t.Fatalf("expected one chart to match regex, got %d", len(charts))
+		}
+	}
+}
+
+func TestListChartsWithShouldNotFindRegex(t *testing.T) {
+	if TestArchiveBucket != "" {
+		tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		charts, err := tr.ListCharts(TestShouldNotFindRegex)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(charts) != 0 {
+			t.Fatalf("expected zero charts to match regex, got %d", len(charts))
+		}
+	}
+}
+
+func TestGetChart(t *testing.T) {
+	if TestArchiveBucket != "" {
+		tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tc, err := tr.GetChart(TestArchiveName)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		have := tc.Chartfile()
+		want, err := chart.LoadChartfile(TestChartFile)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if reflect.DeepEqual(want, have) {
+			t.Fatalf("retrieved an invalid chart\nwant:%#v\nhave:\n%#v\n", want, have)
+		}
+	}
+}
+
+func TestGetChartWithInvalidName(t *testing.T) {
+	var invalidURL = "https://bucket"
+	_, err := NewGCSRepo("testName", invalidURL, nil)
+	if err == nil {
+		t.Fatalf("expected error did not occur for invalid URL")
+	}
+}
diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go
new file mode 100644
index 0000000000000000000000000000000000000000..d9cee38a651c10f166d6297670e3fe6d9f63b8cc
--- /dev/null
+++ b/pkg/repo/repo.go
@@ -0,0 +1,99 @@
+/*
+Copyright 2015 The Kubernetes Authors All rights reserved.
+
+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 repo
+
+import (
+	"github.com/kubernetes/helm/pkg/chart"
+	"github.com/kubernetes/helm/pkg/common"
+
+	"fmt"
+	"net/url"
+	"regexp"
+)
+
+// ChartRepo abstracts a place that holds charts, which can be
+// used in a Deployment Manager configuration. There can be multiple
+// ChartRepo implementations.
+type ChartRepo interface {
+	// GetRepoName returns the name of this ChartRepo.
+	GetRepoName() string
+	// GetRepoType returns the type of this repo.
+	GetRepoType() common.RepoType
+	// GetRepoURL returns the URL to the root of this ChartRepo.
+	GetRepoURL() string
+	// GetRepoFormat returns the format of this ChartRepo.
+	GetRepoFormat() common.RepoFormat
+
+	// ListCharts lists charts in this repository whose string values
+	// conform to the supplied regular expression or all charts if regex is nil
+	ListCharts(regex *regexp.Regexp) ([]string, error)
+	// GetChart retrieves, unpacks and returns a chart by name.
+	GetChart(name string) (*chart.Chart, error)
+}
+
+// ObjectStorageRepo abstracts a repository that resides in an Object Storage, for
+// example Google Cloud Storage or AWS S3, etc.
+type ObjectStorageRepo interface {
+	ChartRepo // An ObjectStorageRepo is a ChartRepo
+	GetBucket() string
+}
+
+type chartRepo struct {
+	Name   string            `json:"name,omitempty"`   // The name of this ChartRepo
+	URL    string            `json:"url,omitempty"`    // The URL to the root of this ChartRepo
+	Format common.RepoFormat `json:"format,omitempty"` // The format of this ChartRepo
+	Type   common.RepoType   `json:"type,omitempty"`   // The type of this ChartRepo
+}
+
+// ChartNameMatcher matches the chart name format
+var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz")
+
+func newRepo(name, URL, format, t string) (*chartRepo, error) {
+	_, err := url.Parse(URL)
+	if err != nil {
+		return nil, fmt.Errorf("invalid URL (%s): %s", URL, err)
+	}
+
+	result := &chartRepo{
+		Name:   name,
+		URL:    URL,
+		Format: common.RepoFormat(format),
+		Type:   common.RepoType(t),
+	}
+
+	return result, nil
+}
+
+// GetRepoName returns the name of this ChartRepo.
+func (cr *chartRepo) GetRepoName() string {
+	return cr.Name
+}
+
+// GetRepoType returns the type of this repo.
+func (cr *chartRepo) GetRepoType() common.RepoType {
+	return cr.Type
+}
+
+// GetRepoURL returns the URL to the root of this ChartRepo.
+func (cr *chartRepo) GetRepoURL() string {
+	return cr.URL
+}
+
+// GetRepoFormat returns the format of this ChartRepo.
+func (cr *chartRepo) GetRepoFormat() common.RepoFormat {
+	return cr.Format
+}
diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f4bc608864cc2f1707346a6db60673821438acab
--- /dev/null
+++ b/pkg/repo/repo_test.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+
+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 repo
+
+import (
+	"testing"
+)
+
+func TestValidURL(t *testing.T) {
+	var wantName = "wantName"
+	var wantType = "wantType"
+	var validURL = "http://valid/url"
+	var wantFormat = "wantFormat"
+
+	tr, err := newRepo(wantName, validURL, wantFormat, wantType)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	haveName := tr.GetRepoName()
+	if haveName != wantName {
+		t.Fatalf("unexpected repo name; want: %s, have %s.", wantName, haveName)
+	}
+
+	haveType := string(tr.GetRepoType())
+	if haveType != wantType {
+		t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType)
+	}
+
+	haveURL := tr.GetRepoURL()
+	if haveURL != validURL {
+		t.Fatalf("unexpected repo url; want: %s, have %s.", validURL, haveURL)
+	}
+
+	haveFormat := string(tr.GetRepoFormat())
+	if haveFormat != wantFormat {
+		t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
+	}
+}
+
+func TestInvalidURL(t *testing.T) {
+	_, err := newRepo("testName", "%:invalid&url:%", "testFormat", "testType")
+	if err == nil {
+		t.Fatalf("expected error did not occur for invalid URL")
+	}
+}
diff --git a/pkg/repo/testdata/README.md b/pkg/repo/testdata/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2f58d9774544c0c597acc9af5cf0e22e3883282f
--- /dev/null
+++ b/pkg/repo/testdata/README.md
@@ -0,0 +1,6 @@
+The testdata directory here holds charts that match the specification.
+
+The `fromnitz/` directory contains a chart that matches the chart
+specification.
+
+The `frobnitz-0.0.1.tgz` file is an archive of the `frobnitz` directory.
diff --git a/pkg/repo/testdata/frobnitz-0.0.1.tgz b/pkg/repo/testdata/frobnitz-0.0.1.tgz
new file mode 100644
index 0000000000000000000000000000000000000000..322f463969025c897ec52b7f94fc44f98d37093d
Binary files /dev/null and b/pkg/repo/testdata/frobnitz-0.0.1.tgz differ
diff --git a/pkg/repo/testdata/frobnitz/Chart.yaml b/pkg/repo/testdata/frobnitz/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b1e67a038f8cfca9bf6158d35e85db44068ddae4
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/Chart.yaml
@@ -0,0 +1,33 @@
+#helm:generate foo
+name: frobnitz
+description: This is a frobniz.
+version: "1.2.3-alpha.1+12345"
+keywords:
+  - frobnitz
+  - sprocket
+  - dodad
+maintainers:
+  - name: The Helm Team
+    email: helm@example.com
+  - name: Someone Else
+    email: nobody@example.com
+source:
+  - https://example.com/foo/bar
+home: http://example.com
+dependencies:
+  - name: thingerbob
+    location: https://example.com/charts/thingerbob-3.2.1.tgz
+    version: ^3
+environment:
+  - name: Kubernetes
+    version: ~1.1
+    extensions:
+      - extensions/v1beta1
+      - extensions/v1beta1/daemonset
+    apiGroups:
+      - 3rdParty
+expander:
+  name: Expandybird
+  entrypoint: templates/wordpress.jinja
+schema: wordpress.jinja.schema
+  
\ No newline at end of file
diff --git a/pkg/repo/testdata/frobnitz/LICENSE b/pkg/repo/testdata/frobnitz/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..6121943b10a68319b2c1f66e659b8a69a4df7ed0
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/pkg/repo/testdata/frobnitz/README.md b/pkg/repo/testdata/frobnitz/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8cf4cc3d7c0f1d3c418e7ba4eab1335b606f063d
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/pkg/repo/testdata/frobnitz/docs/README.md b/pkg/repo/testdata/frobnitz/docs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d40747cafd28860f06babbf27e85d56203e53e35
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/pkg/repo/testdata/frobnitz/hooks/pre-install.py b/pkg/repo/testdata/frobnitz/hooks/pre-install.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9b0d0a9211271c95e4c4632e9780ded4f477fc4
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/hooks/pre-install.py
@@ -0,0 +1 @@
+# Placeholder.
diff --git a/pkg/repo/testdata/frobnitz/icon.svg b/pkg/repo/testdata/frobnitz/icon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8921306066db91867ee2fd152c83ce75785c7803
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/icon.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    version="1.0" width="256" height="256" id="test">
+  <desc>Example icon</desc>
+  <rect id="first" x="2" y="2" width="40" height="60" fill="navy"/>
+  <rect id="second" x="15" y="4" width="40" height="60" fill="red"/>
+</svg>
diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress-resources.yaml b/pkg/repo/testdata/frobnitz/templates/wordpress-resources.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..00f709de04d5aabc6706573f4b0ba604edfe7ec9
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/templates/wordpress-resources.yaml
@@ -0,0 +1,12 @@
+# Google Cloud Deployment Manager template
+resources:
+- name: nfs-disk
+  type: compute.v1.disk
+  properties:
+    zone: us-central1-b
+    sizeGb: 200
+- name: mysql-disk
+  type: compute.v1.disk
+  properties:
+    zone: us-central1-b
+    sizeGb: 200
diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress.jinja b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..f34e4fec91b2a69ba6d8d0de0541df5c89f4e444
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja
@@ -0,0 +1,72 @@
+#helm:generate dm_template
+{% set PROPERTIES = properties or {} %}
+{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %}
+{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %}
+{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %}
+{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %}
+{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %}
+{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %}
+{% set NGINX = PROPERTIES['nginx'] or {} %}
+{% set NGINX_PORT = 80 %}
+{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %}
+{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %}
+{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %}
+{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %}
+{% set MYSQL = PROPERTIES['mysql'] or {} %}
+{% set MYSQL_PORT = MYSQL['port'] or 3306 %}
+{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %}
+{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %}
+{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %}
+
+resources:
+- name: nfs
+  type: github.com/kubernetes/application-dm-templates/storage/nfs:v1
+  properties:
+    ip: {{ NFS_SERVER_IP }}
+    port: {{ NFS_SERVER_PORT }}
+    disk: {{ NFS_SERVER_DISK }}
+    fstype: {{NFS_SERVER_DISK_FSTYPE }}
+- name: nginx
+  type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
+  properties:
+    service_port: {{ NGINX_PORT }}
+    container_port: {{ NGINX_PORT }}
+    replicas: {{ NGINX_REPLICAS }}
+    external_service: true
+    image: gcr.io/{{ PROJECT }}/nginx:latest
+    volumes:
+      - mount_path: /var/www/html
+        persistentVolumeClaim:
+          claimName: nfs
+- name: mysql
+  type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
+  properties:
+    service_port: {{ MYSQL_PORT }}
+    container_port: {{ MYSQL_PORT }}
+    replicas: 1
+    image: mysql:5.6
+    env:
+      - name: MYSQL_ROOT_PASSWORD
+        value: {{ MYSQL_PASSWORD }}
+    volumes:
+      - mount_path: /var/lib/mysql
+        gcePersistentDisk:
+          pdName: {{ MYSQL_DISK }}
+          fsType: {{ MYSQL_DISK_FSTYPE }}
+- name: wordpress-php
+  type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
+  properties:
+    service_name: wordpress-php
+    service_port: {{ WORDPRESS_PHP_PORT }}
+    container_port: {{ WORDPRESS_PHP_PORT }}
+    replicas: 2
+    image: wordpress:fpm
+    env:
+      - name: WORDPRESS_DB_PASSWORD
+        value: {{ MYSQL_PASSWORD }}
+      - name: WORDPRESS_DB_HOST
+        value: mysql-service
+    volumes:
+      - mount_path: /var/www/html
+        persistentVolumeClaim:
+          claimName: nfs
diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress.jinja.schema b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja.schema
new file mode 100644
index 0000000000000000000000000000000000000000..215b47e1e397c106915cba6dc4685e69830425e6
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja.schema
@@ -0,0 +1,69 @@
+info:
+  title: Wordpress
+  description: |
+    Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service.
+
+    The nginx service and the Wordpress-php service both use NFS to share files.
+
+properties:
+  project:
+    type: string
+    default: dm-k8s-testing
+    description: Project location to load the images from.
+  nfs-service:
+    type: object
+    properties:
+      ip:
+        type: string
+        default: 10.0.253.247
+        description: The IP of the NFS service.
+      port:
+        type: int
+        default: 2049
+        description: The port of the NFS service.
+      disk:
+        type: string
+        default: nfs-disk
+        description: The name of the persistent disk the NFS service uses.
+      fstype:
+        type: string
+        default: ext4
+        description: The filesystem the disk of the NFS service uses.
+  nginx:
+    type: object
+    properties:
+      replicas:
+        type: int
+        default: 2
+        description: The number of replicas for the nginx service.
+  wordpress-php:
+    type: object
+    properties:
+      replicas:
+        type: int
+        default: 2
+        description: The number of replicas for the wordpress-php service.
+      port:
+        type: int
+        default: 9000
+        description: The port the wordpress-php service runs on.
+  mysql:
+    type: object
+    properties:
+      port:
+        type: int
+        default: 3306
+        description: The port the MySQL service runs on.
+      password:
+        type: string
+        default: mysql-password
+        description: The root password of the MySQL service.
+      disk:
+        type: string
+        default: mysql-disk
+        description: The name of the persistent disk the MySQL service uses.
+      fstype:
+        type: string
+        default: ext4
+        description: The filesystem the disk of the MySQL service uses.
+
diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress.yaml b/pkg/repo/testdata/frobnitz/templates/wordpress.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b401897abbc3934e2436360681b01ac028125146
--- /dev/null
+++ b/pkg/repo/testdata/frobnitz/templates/wordpress.yaml
@@ -0,0 +1,6 @@
+imports:
+- path: wordpress.jinja
+
+resources:
+- name: wordpress
+  type: wordpress.jinja