Commit fc89743d authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #47353 from apelisse/http-cache

Automatic merge from submit-queue go-client: Use httpcache client for all requests, even though only openapi returns ETags for caching **What this PR does / why we need it**: Use HTTP ETag for caching Swagger spec download This also adds a new command-line flag "cachedir" to specify where the cache should keep its file. It defaults to `$HOME/.kube/http-cache`. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: partly #38637 **Special notes for your reviewer**: Because this adds a bunch of dependencies, and removes a couple of files, I do recommend reading each commit individually. **Release note**: ```release-note ```
parents 29ab38e8 d6348cc1
......@@ -1708,6 +1708,14 @@
"Rev": "6eb6ad425a89d9da7a5549bc6da8f79ba5c17844"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
"Comment": "v1.1-4-g2500245",
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
......@@ -2240,6 +2248,11 @@
"Rev": "0049ab3dc4c4c70a9eee23087437b69c0dde2130"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Comment": "v2.0.0-2-g5dfcb07",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/pkg/errors",
"Comment": "v0.7.0-13-ga221380",
"Rev": "a22138067af1c4942683050411a841ade67fe1eb"
......
......@@ -59442,6 +59442,34 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
= vendor/github.com/gregjones/httpcache licensed under: =
Copyright © 2012 Greg Jones (greg.jones@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
= vendor/github.com/gregjones/httpcache/LICENSE.txt 3cfef421226b2dacde78a4871380ac24 -
================================================================================
================================================================================
= vendor/github.com/gregjones/httpcache/diskcache licensed under: =
Copyright © 2012 Greg Jones (greg.jones@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
= vendor/github.com/gregjones/httpcache/LICENSE.txt 3cfef421226b2dacde78a4871380ac24 -
================================================================================
================================================================================
= vendor/github.com/grpc-ecosystem/go-grpc-prometheus licensed under: =
Apache License
......@@ -71575,6 +71603,33 @@ SOFTWARE.
================================================================================
= vendor/github.com/peterbourgon/diskv licensed under: =
Copyright (c) 2011-2012 Peter Bourgon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
= vendor/github.com/peterbourgon/diskv/LICENSE f9f3e815fc84aa04c4f4db33c553eef9 -
================================================================================
================================================================================
= vendor/github.com/pkg/errors licensed under: =
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
......@@ -138,7 +138,6 @@ func NewCmdGet(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Comman
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
cmdutil.AddInclude3rdPartyFlags(cmd)
cmdutil.AddOpenAPIFlags(cmd)
cmd.Flags().StringVar(&options.Raw, "raw", options.Raw, "Raw URI to request from the server. Uses the transport specified by the kubeconfig file.")
return cmd
}
......@@ -457,7 +456,7 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
// if cmd does not specify output format and useOpenAPIPrintColumnFlagLabel flag is true,
// then get the default output options for this mapping from OpenAPI schema.
if !cmdSpecifiesOutputFmt(cmd) && useOpenAPIPrintColumns {
outputOpts, _ = outputOptsForMappingFromOpenAPI(f, cmdutil.GetOpenAPICacheDir(cmd), mapping)
outputOpts, _ = outputOptsForMappingFromOpenAPI(f, mapping)
}
printer, err = f.PrinterForMapping(cmd, false, outputOpts, mapping, allNamespaces)
......@@ -556,11 +555,11 @@ func cmdSpecifiesOutputFmt(cmd *cobra.Command) bool {
// outputOptsForMappingFromOpenAPI looks for the output format metatadata in the
// openapi schema and returns the output options for the mapping if found.
func outputOptsForMappingFromOpenAPI(f cmdutil.Factory, openAPIcacheDir string, mapping *meta.RESTMapping) (*printers.OutputOptions, bool) {
func outputOptsForMappingFromOpenAPI(f cmdutil.Factory, mapping *meta.RESTMapping) (*printers.OutputOptions, bool) {
// user has not specified any output format, check if OpenAPI has
// default specification to print this resource type
api, err := f.OpenAPISchema(openAPIcacheDir)
api, err := f.OpenAPISchema()
if err != nil {
// Error getting schema
return nil, false
......
......@@ -418,7 +418,7 @@ func (f *FakeFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclar
return nil, nil
}
func (f *FakeFactory) OpenAPISchema(cacheDir string) (openapi.Resources, error) {
func (f *FakeFactory) OpenAPISchema() (openapi.Resources, error) {
return nil, nil
}
......@@ -756,7 +756,7 @@ func (f *fakeAPIFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDec
return nil, nil
}
func (f *fakeAPIFactory) OpenAPISchema(cacheDir string) (openapi.Resources, error) {
func (f *fakeAPIFactory) OpenAPISchema() (openapi.Resources, error) {
if f.tf.OpenAPISchemaFunc != nil {
return f.tf.OpenAPISchemaFunc()
}
......
......@@ -224,7 +224,7 @@ type ObjectMappingFactory interface {
// SwaggerSchema returns the schema declaration for the provided group version kind.
SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclaration, error)
// OpenAPISchema returns the schema openapi schema definiton
OpenAPISchema(cacheDir string) (openapi.Resources, error)
OpenAPISchema() (openapi.Resources, error)
}
// BuilderFactory holds the second level of factory methods. These functions depend upon ObjectMappingFactory and ClientAccessFactory methods.
......
......@@ -439,13 +439,7 @@ func (f *ring1Factory) SwaggerSchema(gvk schema.GroupVersionKind) (*swagger.ApiD
}
// OpenAPISchema returns metadata and structural information about Kubernetes object definitions.
// Will try to cache the data to a local file. Cache is written and read from a
// file created with ioutil.TempFile and obeys the expiration semantics of that file.
// The cache location is a function of the client and server versions so that the open API
// schema will be cached separately for different client / server combinations.
// Note, the cache will not be invalidated if the server changes its open API schema without
// changing the server version.
func (f *ring1Factory) OpenAPISchema(cacheDir string) (openapi.Resources, error) {
func (f *ring1Factory) OpenAPISchema() (openapi.Resources, error) {
discovery, err := f.clientAccessFactory.DiscoveryClient()
if err != nil {
return nil, err
......@@ -453,23 +447,8 @@ func (f *ring1Factory) OpenAPISchema(cacheDir string) (openapi.Resources, error)
// Lazily initialize the OpenAPIGetter once
f.openAPIGetter.once.Do(func() {
// Get the server version for caching the openapi spec
versionString := ""
version, err := discovery.ServerVersion()
if err != nil {
// Cache the result under the server version
versionString = version.String()
}
// Get the cache directory for caching the openapi spec
cacheDir, err = substituteUserHome(cacheDir)
if err != nil {
// Don't cache the result if we couldn't substitute the home directory
cacheDir = ""
}
// Create the caching OpenAPIGetter
f.openAPIGetter.getter = openapi.NewOpenAPIGetter(cacheDir, versionString, discovery)
f.openAPIGetter.getter = openapi.NewOpenAPIGetter(discovery)
})
// Delegate to the OpenAPIGetter
......
......@@ -404,19 +404,6 @@ func AddValidateOptionFlags(cmd *cobra.Command, options *ValidateOptions) {
cmd.MarkFlagFilename("schema-cache-dir")
}
func AddOpenAPIFlags(cmd *cobra.Command) {
cmd.Flags().String("schema-cache-dir",
fmt.Sprintf("~/%s/%s", clientcmd.RecommendedHomeDir, clientcmd.RecommendedSchemaName),
fmt.Sprintf("If non-empty, load/store cached API schemas in this directory, default is '$HOME/%s/%s'",
clientcmd.RecommendedHomeDir, clientcmd.RecommendedSchemaName),
)
cmd.MarkFlagFilename("schema-cache-dir")
}
func GetOpenAPICacheDir(cmd *cobra.Command) string {
return GetFlagString(cmd, "schema-cache-dir")
}
func AddFilenameOptionFlags(cmd *cobra.Command, options *resource.FilenameOptions, usage string) {
kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, "Filename, directory, or URL to files "+usage)
cmd.Flags().BoolVarP(&options.Recursive, "recursive", "R", options.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")
......
......@@ -15,15 +15,11 @@ go_library(
"document.go",
"extensions.go",
"openapi.go",
"openapi_cache.go",
"openapi_getter.go",
],
tags = ["automanaged"],
deps = [
"//pkg/version:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/golang/protobuf/proto:go_default_library",
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
"//vendor/gopkg.in/yaml.v2:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
......@@ -35,7 +31,6 @@ go_test(
name = "go_default_xtest",
size = "small",
srcs = [
"openapi_cache_test.go",
"openapi_getter_test.go",
"openapi_suite_test.go",
"openapi_test.go",
......
/*
Copyright 2017 The Kubernetes 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 openapi
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/golang/glog"
"github.com/golang/protobuf/proto"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
"k8s.io/client-go/discovery"
"k8s.io/kubernetes/pkg/version"
)
const openapiFileName = "openapi_cache"
type CachingOpenAPIClient struct {
version string
client discovery.OpenAPISchemaInterface
cacheDirName string
}
// NewCachingOpenAPIClient returns a new discovery.OpenAPISchemaInterface
// that will read the openapi spec from a local cache if it exists, and
// if not will then fetch an openapi spec using a client.
// client: used to fetch a new openapi spec if a local cache is not found
// version: the server version and used as part of the cache file location
// cacheDir: the directory under which the cache file will be written
func NewCachingOpenAPIClient(client discovery.OpenAPISchemaInterface, version, cacheDir string) *CachingOpenAPIClient {
return &CachingOpenAPIClient{
client: client,
version: version,
cacheDirName: cacheDir,
}
}
// OpenAPIData returns an openapi spec.
// It will first attempt to read the spec from a local cache
// If it cannot read a local cache, it will read the file
// using the client and then write the cache.
func (c *CachingOpenAPIClient) OpenAPIData() (Resources, error) {
// Try to use the cached version
if c.useCache() {
doc, err := c.readOpenAPICache()
if err == nil {
return NewOpenAPIData(doc)
}
}
// No cached version found, download from server
s, err := c.client.OpenAPISchema()
if err != nil {
glog.V(2).Infof("Failed to download openapi data %v", err)
return nil, err
}
oa, err := NewOpenAPIData(s)
if err != nil {
glog.V(2).Infof("Failed to parse openapi data %v", err)
return nil, err
}
// Try to cache the openapi spec
if c.useCache() {
err = c.writeToCache(s)
if err != nil {
// Just log an message, no need to fail the command since we got the data we need
glog.V(2).Infof("Unable to cache openapi spec %v", err)
}
}
// Return the parsed data
return oa, nil
}
// useCache returns true if the client should try to use the cache file
func (c *CachingOpenAPIClient) useCache() bool {
return len(c.version) > 0 && len(c.cacheDirName) > 0
}
// readOpenAPICache tries to read the openapi spec from the local file cache
func (c *CachingOpenAPIClient) readOpenAPICache() (*openapi_v2.Document, error) {
// Get the filename to read
filename := c.openAPICacheFilename()
// Read the cached file
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
doc := &openapi_v2.Document{}
return doc, proto.Unmarshal(data, doc)
}
// writeToCache tries to write the openapi spec to the local file cache.
// writes the data to a new tempfile, and then links the cache file and the tempfile
func (c *CachingOpenAPIClient) writeToCache(doc *openapi_v2.Document) error {
// Get the constant filename used to read the cache.
cacheFile := c.openAPICacheFilename()
// Binary encode the spec. This is 10x as fast as using json encoding. (60ms vs 600ms)
b, err := proto.Marshal(doc)
if err != nil {
return fmt.Errorf("Could not binary encode openapi spec: %v", err)
}
// Create a new temp file for the cached openapi spec.
cacheDir := filepath.Dir(cacheFile)
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("Could not create directory: %v %v", cacheDir, err)
}
tmpFile, err := ioutil.TempFile(cacheDir, "openapi")
if err != nil {
return fmt.Errorf("Could not create temp cache file: %v %v", cacheFile, err)
}
// Write the binary encoded openapi spec to the temp file
if _, err := io.Copy(tmpFile, bytes.NewBuffer(b)); err != nil {
return fmt.Errorf("Could not write temp cache file: %v", err)
}
// Link the temp cache file to the constant cache filepath
return linkFiles(tmpFile.Name(), cacheFile)
}
// openAPICacheFilename returns the filename to read the cache from
func (c *CachingOpenAPIClient) openAPICacheFilename() string {
// Cache using the client and server versions
return filepath.Join(c.cacheDirName, c.version, version.Get().GitVersion, openapiFileName)
}
// linkFiles links the old file to the new file
func linkFiles(old, new string) error {
if err := os.Link(old, new); err != nil {
// If we can't write due to file existing, or permission problems, keep going.
if os.IsExist(err) || os.IsPermission(err) {
return nil
}
return err
}
return nil
}
/*
Copyright 2017 The Kubernetes 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 openapi_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v2"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/googleapis/gnostic/compiler"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
)
var _ = Describe("When reading openAPIData", func() {
var tmpDir string
var err error
var client *fakeOpenAPIClient
var instance *openapi.CachingOpenAPIClient
var expectedData openapi.Resources
BeforeEach(func() {
tmpDir, err = ioutil.TempDir("", "openapi_cache_test")
Expect(err).To(BeNil())
client = &fakeOpenAPIClient{}
instance = openapi.NewCachingOpenAPIClient(client, "v1.6", tmpDir)
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
})
AfterEach(func() {
os.RemoveAll(tmpDir)
})
It("should write to the cache", func() {
By("getting the live openapi spec from the server")
result, err := instance.OpenAPIData()
Expect(err).To(BeNil())
Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1))
By("writing the live openapi spec to a local cache file")
names, err := getFilenames(tmpDir)
Expect(err).To(BeNil())
Expect(names).To(ConsistOf("v1.6"))
names, err = getFilenames(filepath.Join(tmpDir, "v1.6"))
Expect(err).To(BeNil())
Expect(names).To(HaveLen(1))
clientVersion := names[0]
names, err = getFilenames(filepath.Join(tmpDir, "v1.6", clientVersion))
Expect(err).To(BeNil())
Expect(names).To(ContainElement("openapi_cache"))
})
It("should read from the cache", func() {
// First call should use the client
result, err := instance.OpenAPIData()
Expect(err).To(BeNil())
Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1))
// Second call shouldn't use the client
result, err = instance.OpenAPIData()
Expect(err).To(BeNil())
Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1))
names, err := getFilenames(tmpDir)
Expect(err).To(BeNil())
Expect(names).To(ConsistOf("v1.6"))
})
It("propagate errors that are encountered", func() {
// Expect an error
client.err = fmt.Errorf("expected error")
result, err := instance.OpenAPIData()
Expect(err.Error()).To(Equal(client.err.Error()))
Expect(result).To(BeNil())
Expect(client.calls).To(Equal(1))
// No cache file is written
files, err := ioutil.ReadDir(tmpDir)
Expect(err).To(BeNil())
Expect(files).To(HaveLen(0))
// Client error is not cached
result, err = instance.OpenAPIData()
Expect(err.Error()).To(Equal(client.err.Error()))
Expect(result).To(BeNil())
Expect(client.calls).To(Equal(2))
})
})
var _ = Describe("Reading openAPIData", func() {
var tmpDir string
var serverVersion string
var cacheDir string
BeforeEach(func() {
var err error
tmpDir, err = ioutil.TempDir("", "openapi_cache_test")
Expect(err).To(BeNil())
})
AfterEach(func() {
os.RemoveAll(tmpDir)
})
// Set the serverVersion to empty
Context("when the server version is empty", func() {
BeforeEach(func() {
serverVersion = ""
cacheDir = tmpDir
})
It("should not cache the result", func() {
client := &fakeOpenAPIClient{}
instance := openapi.NewCachingOpenAPIClient(client, serverVersion, cacheDir)
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err := openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
By("getting the live openapi schema")
result, err := instance.OpenAPIData()
Expect(err).To(BeNil())
Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1))
files, err := ioutil.ReadDir(tmpDir)
Expect(err).To(BeNil())
Expect(files).To(HaveLen(0))
})
})
Context("when the cache directory is empty", func() {
BeforeEach(func() {
serverVersion = "v1.6"
cacheDir = ""
})
It("should not cache the result", func() {
client := &fakeOpenAPIClient{}
instance := openapi.NewCachingOpenAPIClient(client, serverVersion, cacheDir)
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err := openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
By("getting the live openapi schema")
result, err := instance.OpenAPIData()
Expect(err).To(BeNil())
Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1))
files, err := ioutil.ReadDir(tmpDir)
Expect(err).To(BeNil())
Expect(files).To(HaveLen(0))
})
})
})
// Test Utils
func getFilenames(path string) ([]string, error) {
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
result := []string{}
for _, n := range files {
result = append(result, n.Name())
}
return result, nil
}
type fakeOpenAPIClient struct {
calls int
err error
}
func (f *fakeOpenAPIClient) OpenAPISchema() (*openapi_v2.Document, error) {
f.calls = f.calls + 1
if f.err != nil {
return nil, f.err
}
return data.OpenAPISchema()
}
// Test utils
var data apiData
type apiData struct {
sync.Once
data *openapi_v2.Document
err error
}
func (d *apiData) OpenAPISchema() (*openapi_v2.Document, error) {
d.Do(func() {
// Get the path to the swagger.json file
wd, err := os.Getwd()
if err != nil {
d.err = err
return
}
abs, err := filepath.Abs(wd)
if err != nil {
d.err = err
return
}
root := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(abs)))))
specpath := filepath.Join(root, "api", "openapi-spec", "swagger.json")
_, err = os.Stat(specpath)
if err != nil {
d.err = err
return
}
spec, err := ioutil.ReadFile(specpath)
if err != nil {
d.err = err
return
}
var info yaml.MapSlice
err = yaml.Unmarshal(spec, &info)
if err != nil {
d.err = err
return
}
d.data, d.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
})
return d.data, d.err
}
......@@ -29,8 +29,6 @@ type synchronizedOpenAPIGetter struct {
openAPISchema Resources
err error
serverVersion string
cacheDir string
openAPIClient discovery.OpenAPISchemaInterface
}
......@@ -42,12 +40,10 @@ type Getter interface {
Get() (Resources, error)
}
// NewOpenAPIGetter returns an object to return OpenAPIDatas which either read from a
// local file cache or read from a server, and then stored in memory for subsequent invocations
func NewOpenAPIGetter(cacheDir, serverVersion string, openAPIClient discovery.OpenAPISchemaInterface) Getter {
// NewOpenAPIGetter returns an object to return OpenAPIDatas which reads
// from a server, and then stores in memory for subsequent invocations
func NewOpenAPIGetter(openAPIClient discovery.OpenAPISchemaInterface) Getter {
return &synchronizedOpenAPIGetter{
serverVersion: serverVersion,
cacheDir: cacheDir,
openAPIClient: openAPIClient,
}
}
......@@ -55,15 +51,13 @@ func NewOpenAPIGetter(cacheDir, serverVersion string, openAPIClient discovery.Op
// Resources implements Getter
func (g *synchronizedOpenAPIGetter) Get() (Resources, error) {
g.Do(func() {
client := NewCachingOpenAPIClient(g.openAPIClient, g.serverVersion, g.cacheDir)
result, err := client.OpenAPIData()
s, err := g.openAPIClient.OpenAPISchema()
if err != nil {
g.err = err
return
}
// Save the result
g.openAPISchema = result
g.openAPISchema, g.err = NewOpenAPIData(s)
})
// Return the save result
......
......@@ -18,13 +18,83 @@ package openapi_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v2"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/googleapis/gnostic/compiler"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
)
// Test utils
var data apiData
type apiData struct {
sync.Once
data *openapi_v2.Document
err error
}
func (d *apiData) OpenAPISchema() (*openapi_v2.Document, error) {
d.Do(func() {
// Get the path to the swagger.json file
wd, err := os.Getwd()
if err != nil {
d.err = err
return
}
abs, err := filepath.Abs(wd)
if err != nil {
d.err = err
return
}
root := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(abs)))))
specpath := filepath.Join(root, "api", "openapi-spec", "swagger.json")
_, err = os.Stat(specpath)
if err != nil {
d.err = err
return
}
spec, err := ioutil.ReadFile(specpath)
if err != nil {
d.err = err
return
}
var info yaml.MapSlice
err = yaml.Unmarshal(spec, &info)
if err != nil {
d.err = err
return
}
d.data, d.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
})
return d.data, d.err
}
type fakeOpenAPIClient struct {
calls int
err error
}
func (f *fakeOpenAPIClient) OpenAPISchema() (*openapi_v2.Document, error) {
f.calls = f.calls + 1
if f.err != nil {
return nil, f.err
}
return data.OpenAPISchema()
}
var _ = Describe("Getting the Resources", func() {
var client *fakeOpenAPIClient
var expectedData openapi.Resources
......@@ -38,7 +108,7 @@ var _ = Describe("Getting the Resources", func() {
expectedData, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
instance = openapi.NewOpenAPIGetter("", "", client)
instance = openapi.NewOpenAPIGetter(client)
})
Context("when the server returns a successful result", func() {
......
......@@ -163,6 +163,10 @@
"Rev": "4bd1920723d7b7c925de087aa32e2187708897f7"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
......@@ -179,6 +183,14 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
},
......@@ -243,6 +255,10 @@
"Rev": "ca53cad383cad2479bbba7f7a1a05797ec1386e4"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/pkg/errors",
"Rev": "a22138067af1c4942683050411a841ade67fe1eb"
},
......
......@@ -431,6 +431,14 @@
"Rev": "ed590d9afe113c6107cd60717b196155e6579e78"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
},
......@@ -499,6 +507,10 @@
"Rev": "ca53cad383cad2479bbba7f7a1a05797ec1386e4"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/pkg/errors",
"Rev": "a22138067af1c4942683050411a841ade67fe1eb"
},
......
......@@ -155,6 +155,10 @@
"Rev": "4bd1920723d7b7c925de087aa32e2187708897f7"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
......@@ -171,6 +175,14 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/hashicorp/golang-lru",
"Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
},
......@@ -207,6 +219,10 @@
"Rev": "d5b7844b561a7bc640052f1b935f7b800330d7e0"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/pmezard/go-difflib/difflib",
"Rev": "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
},
......
......@@ -71,6 +71,10 @@ type Config struct {
// TODO: demonstrate an OAuth2 compatible client.
BearerToken string
// CacheDir is the directory where we'll store HTTP cached responses.
// If set to empty string, no caching mechanism will be used.
CacheDir string
// Impersonate is the configuration that RESTClient will use for impersonation.
Impersonate ImpersonationConfig
......
......@@ -249,6 +249,7 @@ func TestAnonymousConfig(t *testing.T) {
expected.BearerToken = ""
expected.Username = ""
expected.Password = ""
expected.CacheDir = ""
expected.AuthProvider = nil
expected.AuthConfigPersister = nil
expected.TLSClientConfig.CertData = nil
......
......@@ -89,6 +89,7 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
},
Username: c.Username,
Password: c.Password,
CacheDir: c.CacheDir,
BearerToken: c.BearerToken,
Impersonate: transport.ImpersonationConfig{
UserName: c.Impersonate.UserName,
......
......@@ -22,6 +22,7 @@ import (
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
......@@ -31,16 +32,19 @@ import (
restclient "k8s.io/client-go/rest"
clientauth "k8s.io/client-go/tools/auth"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/homedir"
)
var (
// ClusterDefaults has the same behavior as the old EnvVar and DefaultCluster fields
// DEPRECATED will be replaced
ClusterDefaults = clientcmdapi.Cluster{Server: getDefaultServer()}
cacheDirDefault = filepath.Join(homedir.HomeDir(), ".kube", "http-cache")
// DefaultClientConfig represents the legacy behavior of this package for defaulting
// DEPRECATED will be replace
DefaultClientConfig = DirectClientConfig{*clientcmdapi.NewConfig(), "", &ConfigOverrides{
ClusterDefaults: ClusterDefaults,
CacheDir: cacheDirDefault,
}, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}}
)
......@@ -131,6 +135,7 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
clientConfig := &restclient.Config{}
clientConfig.Host = configClusterInfo.Server
clientConfig.CacheDir = config.overrides.CacheDir
if len(config.overrides.Timeout) > 0 {
timeout, err := ParseTimeout(config.overrides.Timeout)
......
......@@ -17,11 +17,13 @@ limitations under the License.
package clientcmd
import (
"path/filepath"
"strconv"
"github.com/spf13/pflag"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/homedir"
)
// ConfigOverrides holds values that should override whatever information is pulled from the actual Config object. You can't
......@@ -34,6 +36,7 @@ type ConfigOverrides struct {
Context clientcmdapi.Context
CurrentContext string
Timeout string
CacheDir string
}
// ConfigOverrideFlags holds the flag names to be used for binding command line flags. Notice that this structure tightly
......@@ -44,6 +47,7 @@ type ConfigOverrideFlags struct {
ContextOverrideFlags ContextOverrideFlags
CurrentContext FlagInfo
Timeout FlagInfo
CacheDir FlagInfo
}
// AuthOverrideFlags holds the flag names to be used for binding command line flags for AuthInfo objects
......@@ -146,10 +150,12 @@ const (
FlagUsername = "username"
FlagPassword = "password"
FlagTimeout = "request-timeout"
FlagCacheDir = "cachedir"
)
// RecommendedConfigOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags {
defaultCacheDir := filepath.Join(homedir.HomeDir(), ".kube", "http-cache")
return ConfigOverrideFlags{
AuthOverrideFlags: RecommendedAuthOverrideFlags(prefix),
ClusterOverrideFlags: RecommendedClusterOverrideFlags(prefix),
......@@ -157,6 +163,7 @@ func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags {
CurrentContext: FlagInfo{prefix + FlagContext, "", "", "The name of the kubeconfig context to use"},
Timeout: FlagInfo{prefix + FlagTimeout, "", "0", "The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests."},
CacheDir: FlagInfo{prefix + FlagCacheDir, "", defaultCacheDir, "Path to http-cache directory"},
}
}
......@@ -198,6 +205,7 @@ func BindOverrideFlags(overrides *ConfigOverrides, flags *pflag.FlagSet, flagNam
BindContextFlags(&overrides.Context, flags, flagNames.ContextOverrideFlags)
flagNames.CurrentContext.BindStringFlag(flags, &overrides.CurrentContext)
flagNames.Timeout.BindStringFlag(flags, &overrides.Timeout)
flagNames.CacheDir.BindStringFlag(flags, &overrides.CacheDir)
}
// BindAuthInfoFlags is a convenience method to bind the specified flags to their associated variables
......
......@@ -30,6 +30,9 @@ go_library(
tags = ["automanaged"],
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/gregjones/httpcache:go_default_library",
"//vendor/github.com/gregjones/httpcache/diskcache:go_default_library",
"//vendor/github.com/peterbourgon/diskv:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
],
)
......
......@@ -34,6 +34,10 @@ type Config struct {
// Bearer token for authentication
BearerToken string
// CacheDir is the directory where we'll store HTTP cached responses.
// If set to empty string, no caching mechanism will be used.
CacheDir string
// Impersonate is the config that this Config will impersonate using
Impersonate ImpersonationConfig
......
......@@ -23,6 +23,9 @@ import (
"time"
"github.com/golang/glog"
"github.com/gregjones/httpcache"
"github.com/gregjones/httpcache/diskcache"
"github.com/peterbourgon/diskv"
utilnet "k8s.io/apimachinery/pkg/util/net"
)
......@@ -56,6 +59,9 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip
len(config.Impersonate.Extra) > 0 {
rt = NewImpersonatingRoundTripper(config.Impersonate, rt)
}
if len(config.CacheDir) > 0 {
rt = NewCacheRoundTripper(config.CacheDir, rt)
}
return rt, nil
}
......@@ -87,6 +93,17 @@ type authProxyRoundTripper struct {
rt http.RoundTripper
}
// NewCacheRoundTripper creates a roundtripper that reads the ETag on
// response headers and send the If-None-Match header on subsequent
// corresponding requests.
func NewCacheRoundTripper(cacheDir string, rt http.RoundTripper) http.RoundTripper {
d := diskv.New(diskv.Options{BasePath: cacheDir})
t := httpcache.NewTransport(diskcache.NewWithDiskv(d))
t.Transport = rt
return t
}
// NewAuthProxyRoundTripper provides a roundtripper which will add auth proxy fields to requests for
// authentication terminating proxy cases
// assuming you pull the user from the context:
......
......@@ -17,7 +17,11 @@ limitations under the License.
package transport
import (
"bytes"
"io/ioutil"
"net/http"
"net/url"
"os"
"reflect"
"strings"
"testing"
......@@ -216,3 +220,60 @@ func TestAuthProxyRoundTripper(t *testing.T) {
}
}
}
func TestCacheRoundTripper(t *testing.T) {
rt := &testRoundTripper{}
cacheDir, err := ioutil.TempDir("", "cache-rt")
defer os.RemoveAll(cacheDir)
if err != nil {
t.Fatal(err)
}
cache := NewCacheRoundTripper(cacheDir, rt)
// First call, caches the response
req := &http.Request{
Method: http.MethodGet,
URL: &url.URL{Host: "localhost"},
}
rt.Response = &http.Response{
Header: http.Header{"ETag": []string{`"123456"`}},
Body: ioutil.NopCloser(bytes.NewReader([]byte("Content"))),
StatusCode: http.StatusOK,
}
resp, err := cache.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(content) != "Content" {
t.Errorf(`Expected Body to be "Content", got %q`, string(content))
}
// Second call, returns cached response
req = &http.Request{
Method: http.MethodGet,
URL: &url.URL{Host: "localhost"},
}
rt.Response = &http.Response{
StatusCode: http.StatusNotModified,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Other Content"))),
}
resp, err = cache.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
// Read body and make sure we have the initial content
content, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
t.Fatal(err)
}
if string(content) != "Content" {
t.Errorf("Invalid content read from cache %q", string(content))
}
}
......@@ -171,6 +171,10 @@
"Rev": "4bd1920723d7b7c925de087aa32e2187708897f7"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
......@@ -187,6 +191,14 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
},
......@@ -251,6 +263,10 @@
"Rev": "ca53cad383cad2479bbba7f7a1a05797ec1386e4"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/pkg/errors",
"Rev": "a22138067af1c4942683050411a841ade67fe1eb"
},
......
......@@ -175,6 +175,10 @@
"Rev": "4bd1920723d7b7c925de087aa32e2187708897f7"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
......@@ -191,6 +195,14 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/hashicorp/golang-lru",
"Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
},
......@@ -215,6 +227,10 @@
"Rev": "d5b7844b561a7bc640052f1b935f7b800330d7e0"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/spf13/pflag",
"Rev": "9ff6c6923cfffbcd502984b8e0c80539a94968b7"
},
......
......@@ -83,6 +83,10 @@
"Rev": "4bd1920723d7b7c925de087aa32e2187708897f7"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
......@@ -99,6 +103,14 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/juju/ratelimit",
"Rev": "5b9ff866471762aa2ab2dced63c9fb6f53921342"
},
......@@ -115,6 +127,10 @@
"Rev": "d5b7844b561a7bc640052f1b935f7b800330d7e0"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/spf13/pflag",
"Rev": "9ff6c6923cfffbcd502984b8e0c80539a94968b7"
},
......
......@@ -163,6 +163,10 @@
"Rev": "4bd1920723d7b7c925de087aa32e2187708897f7"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
......@@ -179,6 +183,14 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/gregjones/httpcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/gregjones/httpcache/diskcache",
"Rev": "787624de3eb7bd915c329cba748687a3b22666a6"
},
{
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
},
......@@ -243,6 +255,10 @@
"Rev": "ca53cad383cad2479bbba7f7a1a05797ec1386e4"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "5dfcb07a075adbaaa4094cddfd160b1e1c77a043"
},
{
"ImportPath": "github.com/pkg/errors",
"Rev": "a22138067af1c4942683050411a841ade67fe1eb"
},
......
......@@ -230,6 +230,7 @@ filegroup(
"//vendor/github.com/gorilla/context:all-srcs",
"//vendor/github.com/gorilla/mux:all-srcs",
"//vendor/github.com/gorilla/websocket:all-srcs",
"//vendor/github.com/gregjones/httpcache:all-srcs",
"//vendor/github.com/grpc-ecosystem/go-grpc-prometheus:all-srcs",
"//vendor/github.com/grpc-ecosystem/grpc-gateway/runtime:all-srcs",
"//vendor/github.com/grpc-ecosystem/grpc-gateway/utilities:all-srcs",
......@@ -276,6 +277,7 @@ filegroup(
"//vendor/github.com/pborman/uuid:all-srcs",
"//vendor/github.com/pelletier/go-buffruneio:all-srcs",
"//vendor/github.com/pelletier/go-toml:all-srcs",
"//vendor/github.com/peterbourgon/diskv:all-srcs",
"//vendor/github.com/pkg/errors:all-srcs",
"//vendor/github.com/pkg/sftp:all-srcs",
"//vendor/github.com/pmezard/go-difflib/difflib:all-srcs",
......
sudo: false
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- master
matrix:
allow_failures:
- go: master
fast_finish: true
install:
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go tool vet .
- go test -v -race ./...
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["httpcache.go"],
tags = ["automanaged"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//vendor/github.com/gregjones/httpcache/diskcache:all-srcs",
],
tags = ["automanaged"],
)
Copyright © 2012 Greg Jones (greg.jones@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
httpcache
=========
[![Build Status](https://travis-ci.org/gregjones/httpcache.svg?branch=master)](https://travis-ci.org/gregjones/httpcache) [![GoDoc](https://godoc.org/github.com/gregjones/httpcache?status.svg)](https://godoc.org/github.com/gregjones/httpcache)
Package httpcache provides a http.RoundTripper implementation that works as a mostly RFC-compliant cache for http responses.
It is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client and not for a shared proxy).
Cache Backends
--------------
- The built-in 'memory' cache stores responses in an in-memory map.
- [`github.com/gregjones/httpcache/diskcache`](https://github.com/gregjones/httpcache/tree/master/diskcache) provides a filesystem-backed cache using the [diskv](https://github.com/peterbourgon/diskv) library.
- [`github.com/gregjones/httpcache/memcache`](https://github.com/gregjones/httpcache/tree/master/memcache) provides memcache implementations, for both App Engine and 'normal' memcache servers.
- [`sourcegraph.com/sourcegraph/s3cache`](https://sourcegraph.com/github.com/sourcegraph/s3cache) uses Amazon S3 for storage.
- [`github.com/gregjones/httpcache/leveldbcache`](https://github.com/gregjones/httpcache/tree/master/leveldbcache) provides a filesystem-backed cache using [leveldb](https://github.com/syndtr/goleveldb/leveldb).
- [`github.com/die-net/lrucache`](https://github.com/die-net/lrucache) provides an in-memory cache that will evict least-recently used entries.
- [`github.com/die-net/lrucache/twotier`](https://github.com/die-net/lrucache/tree/master/twotier) allows caches to be combined, for example to use lrucache above with a persistent disk-cache.
License
-------
- [MIT License](LICENSE.txt)
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["diskcache.go"],
tags = ["automanaged"],
deps = ["//vendor/github.com/peterbourgon/diskv:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
// Package diskcache provides an implementation of httpcache.Cache that uses the diskv package
// to supplement an in-memory map with persistent storage
//
package diskcache
import (
"bytes"
"crypto/md5"
"encoding/hex"
"github.com/peterbourgon/diskv"
"io"
)
// Cache is an implementation of httpcache.Cache that supplements the in-memory map with persistent storage
type Cache struct {
d *diskv.Diskv
}
// Get returns the response corresponding to key if present
func (c *Cache) Get(key string) (resp []byte, ok bool) {
key = keyToFilename(key)
resp, err := c.d.Read(key)
if err != nil {
return []byte{}, false
}
return resp, true
}
// Set saves a response to the cache as key
func (c *Cache) Set(key string, resp []byte) {
key = keyToFilename(key)
c.d.WriteStream(key, bytes.NewReader(resp), true)
}
// Delete removes the response with key from the cache
func (c *Cache) Delete(key string) {
key = keyToFilename(key)
c.d.Erase(key)
}
func keyToFilename(key string) string {
h := md5.New()
io.WriteString(h, key)
return hex.EncodeToString(h.Sum(nil))
}
// New returns a new Cache that will store files in basePath
func New(basePath string) *Cache {
return &Cache{
d: diskv.New(diskv.Options{
BasePath: basePath,
CacheSizeMax: 100 * 1024 * 1024, // 100MB
}),
}
}
// NewWithDiskv returns a new Cache using the provided Diskv as underlying
// storage.
func NewWithDiskv(d *diskv.Diskv) *Cache {
return &Cache{d}
}
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"compression.go",
"diskv.go",
"index.go",
],
tags = ["automanaged"],
deps = ["//vendor/github.com/google/btree:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
Copyright (c) 2011-2012 Peter Bourgon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# What is diskv?
Diskv (disk-vee) is a simple, persistent key-value store written in the Go
language. It starts with an incredibly simple API for storing arbitrary data on
a filesystem by key, and builds several layers of performance-enhancing
abstraction on top. The end result is a conceptually simple, but highly
performant, disk-backed storage system.
[![Build Status][1]][2]
[1]: https://drone.io/github.com/peterbourgon/diskv/status.png
[2]: https://drone.io/github.com/peterbourgon/diskv/latest
# Installing
Install [Go 1][3], either [from source][4] or [with a prepackaged binary][5].
Then,
```bash
$ go get github.com/peterbourgon/diskv
```
[3]: http://golang.org
[4]: http://golang.org/doc/install/source
[5]: http://golang.org/doc/install
# Usage
```go
package main
import (
"fmt"
"github.com/peterbourgon/diskv"
)
func main() {
// Simplest transform function: put all the data files into the base dir.
flatTransform := func(s string) []string { return []string{} }
// Initialize a new diskv store, rooted at "my-data-dir", with a 1MB cache.
d := diskv.New(diskv.Options{
BasePath: "my-data-dir",
Transform: flatTransform,
CacheSizeMax: 1024 * 1024,
})
// Write three bytes to the key "alpha".
key := "alpha"
d.Write(key, []byte{'1', '2', '3'})
// Read the value back out of the store.
value, _ := d.Read(key)
fmt.Printf("%v\n", value)
// Erase the key+value from the store (and the disk).
d.Erase(key)
}
```
More complex examples can be found in the "examples" subdirectory.
# Theory
## Basic idea
At its core, diskv is a map of a key (`string`) to arbitrary data (`[]byte`).
The data is written to a single file on disk, with the same name as the key.
The key determines where that file will be stored, via a user-provided
`TransformFunc`, which takes a key and returns a slice (`[]string`)
corresponding to a path list where the key file will be stored. The simplest
TransformFunc,
```go
func SimpleTransform (key string) []string {
return []string{}
}
```
will place all keys in the same, base directory. The design is inspired by
[Redis diskstore][6]; a TransformFunc which emulates the default diskstore
behavior is available in the content-addressable-storage example.
[6]: http://groups.google.com/group/redis-db/browse_thread/thread/d444bc786689bde9?pli=1
**Note** that your TransformFunc should ensure that one valid key doesn't
transform to a subset of another valid key. That is, it shouldn't be possible
to construct valid keys that resolve to directory names. As a concrete example,
if your TransformFunc splits on every 3 characters, then
```go
d.Write("abcabc", val) // OK: written to <base>/abc/abc/abcabc
d.Write("abc", val) // Error: attempted write to <base>/abc/abc, but it's a directory
```
This will be addressed in an upcoming version of diskv.
Probably the most important design principle behind diskv is that your data is
always flatly available on the disk. diskv will never do anything that would
prevent you from accessing, copying, backing up, or otherwise interacting with
your data via common UNIX commandline tools.
## Adding a cache
An in-memory caching layer is provided by combining the BasicStore
functionality with a simple map structure, and keeping it up-to-date as
appropriate. Since the map structure in Go is not threadsafe, it's combined
with a RWMutex to provide safe concurrent access.
## Adding order
diskv is a key-value store and therefore inherently unordered. An ordering
system can be injected into the store by passing something which satisfies the
diskv.Index interface. (A default implementation, using Google's
[btree][7] package, is provided.) Basically, diskv keeps an ordered (by a
user-provided Less function) index of the keys, which can be queried.
[7]: https://github.com/google/btree
## Adding compression
Something which implements the diskv.Compression interface may be passed
during store creation, so that all Writes and Reads are filtered through
a compression/decompression pipeline. Several default implementations,
using stdlib compression algorithms, are provided. Note that data is cached
compressed; the cost of decompression is borne with each Read.
## Streaming
diskv also now provides ReadStream and WriteStream methods, to allow very large
data to be handled efficiently.
# Future plans
* Needs plenty of robust testing: huge datasets, etc...
* More thorough benchmarking
* Your suggestions for use-cases I haven't thought of
package diskv
import (
"compress/flate"
"compress/gzip"
"compress/zlib"
"io"
)
// Compression is an interface that Diskv uses to implement compression of
// data. Writer takes a destination io.Writer and returns a WriteCloser that
// compresses all data written through it. Reader takes a source io.Reader and
// returns a ReadCloser that decompresses all data read through it. You may
// define these methods on your own type, or use one of the NewCompression
// helpers.
type Compression interface {
Writer(dst io.Writer) (io.WriteCloser, error)
Reader(src io.Reader) (io.ReadCloser, error)
}
// NewGzipCompression returns a Gzip-based Compression.
func NewGzipCompression() Compression {
return NewGzipCompressionLevel(flate.DefaultCompression)
}
// NewGzipCompressionLevel returns a Gzip-based Compression with the given level.
func NewGzipCompressionLevel(level int) Compression {
return &genericCompression{
wf: func(w io.Writer) (io.WriteCloser, error) { return gzip.NewWriterLevel(w, level) },
rf: func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },
}
}
// NewZlibCompression returns a Zlib-based Compression.
func NewZlibCompression() Compression {
return NewZlibCompressionLevel(flate.DefaultCompression)
}
// NewZlibCompressionLevel returns a Zlib-based Compression with the given level.
func NewZlibCompressionLevel(level int) Compression {
return NewZlibCompressionLevelDict(level, nil)
}
// NewZlibCompressionLevelDict returns a Zlib-based Compression with the given
// level, based on the given dictionary.
func NewZlibCompressionLevelDict(level int, dict []byte) Compression {
return &genericCompression{
func(w io.Writer) (io.WriteCloser, error) { return zlib.NewWriterLevelDict(w, level, dict) },
func(r io.Reader) (io.ReadCloser, error) { return zlib.NewReaderDict(r, dict) },
}
}
type genericCompression struct {
wf func(w io.Writer) (io.WriteCloser, error)
rf func(r io.Reader) (io.ReadCloser, error)
}
func (g *genericCompression) Writer(dst io.Writer) (io.WriteCloser, error) {
return g.wf(dst)
}
func (g *genericCompression) Reader(src io.Reader) (io.ReadCloser, error) {
return g.rf(src)
}
package diskv
import (
"sync"
"github.com/google/btree"
)
// Index is a generic interface for things that can
// provide an ordered list of keys.
type Index interface {
Initialize(less LessFunction, keys <-chan string)
Insert(key string)
Delete(key string)
Keys(from string, n int) []string
}
// LessFunction is used to initialize an Index of keys in a specific order.
type LessFunction func(string, string) bool
// btreeString is a custom data type that satisfies the BTree Less interface,
// making the strings it wraps sortable by the BTree package.
type btreeString struct {
s string
l LessFunction
}
// Less satisfies the BTree.Less interface using the btreeString's LessFunction.
func (s btreeString) Less(i btree.Item) bool {
return s.l(s.s, i.(btreeString).s)
}
// BTreeIndex is an implementation of the Index interface using google/btree.
type BTreeIndex struct {
sync.RWMutex
LessFunction
*btree.BTree
}
// Initialize populates the BTree tree with data from the keys channel,
// according to the passed less function. It's destructive to the BTreeIndex.
func (i *BTreeIndex) Initialize(less LessFunction, keys <-chan string) {
i.Lock()
defer i.Unlock()
i.LessFunction = less
i.BTree = rebuild(less, keys)
}
// Insert inserts the given key (only) into the BTree tree.
func (i *BTreeIndex) Insert(key string) {
i.Lock()
defer i.Unlock()
if i.BTree == nil || i.LessFunction == nil {
panic("uninitialized index")
}
i.BTree.ReplaceOrInsert(btreeString{s: key, l: i.LessFunction})
}
// Delete removes the given key (only) from the BTree tree.
func (i *BTreeIndex) Delete(key string) {
i.Lock()
defer i.Unlock()
if i.BTree == nil || i.LessFunction == nil {
panic("uninitialized index")
}
i.BTree.Delete(btreeString{s: key, l: i.LessFunction})
}
// Keys yields a maximum of n keys in order. If the passed 'from' key is empty,
// Keys will return the first n keys. If the passed 'from' key is non-empty, the
// first key in the returned slice will be the key that immediately follows the
// passed key, in key order.
func (i *BTreeIndex) Keys(from string, n int) []string {
i.RLock()
defer i.RUnlock()
if i.BTree == nil || i.LessFunction == nil {
panic("uninitialized index")
}
if i.BTree.Len() <= 0 {
return []string{}
}
btreeFrom := btreeString{s: from, l: i.LessFunction}
skipFirst := true
if len(from) <= 0 || !i.BTree.Has(btreeFrom) {
// no such key, so fabricate an always-smallest item
btreeFrom = btreeString{s: "", l: func(string, string) bool { return true }}
skipFirst = false
}
keys := []string{}
iterator := func(i btree.Item) bool {
keys = append(keys, i.(btreeString).s)
return len(keys) < n
}
i.BTree.AscendGreaterOrEqual(btreeFrom, iterator)
if skipFirst && len(keys) > 0 {
keys = keys[1:]
}
return keys
}
// rebuildIndex does the work of regenerating the index
// with the given keys.
func rebuild(less LessFunction, keys <-chan string) *btree.BTree {
tree := btree.New(2)
for key := range keys {
tree.ReplaceOrInsert(btreeString{s: key, l: less})
}
return tree
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment