Skip to content

Commit

Permalink
api: Adding API major.minor.patch version (envoyproxy#15186)
Browse files Browse the repository at this point in the history
This PR adds the API_VERSION to Envoy, and the api version handling class to fetch the latest and oldest supported API versions by Envoy.
This is the first step in implementing the proposal in: envoyproxy#8416.

Risk Level: Low (no usage in the code).
Testing: Unit tests.
Docs Changes: None.
Release Notes: None.
Platform Specific Features: None.

Signed-off-by: Adi Suissa-Peleg <[email protected]>
  • Loading branch information
adisuissa authored Mar 16, 2021
1 parent ae67b9e commit 1d2c26f
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 0 deletions.
1 change: 1 addition & 0 deletions API_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.0.0
1 change: 1 addition & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ licenses(["notice"]) # Apache 2

exports_files([
"VERSION",
"API_VERSION",
".clang-format",
])

Expand Down
23 changes: 23 additions & 0 deletions source/common/version/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ genrule(
visibility = ["//visibility:private"],
)

genrule(
name = "generate_api_version_number",
srcs = ["//:API_VERSION"],
outs = ["api_version_number.h"],
cmd = """./$(location //tools/api_versioning:generate_api_version_header_bin) $< >$@""",
tools = ["//tools/api_versioning:generate_api_version_header_bin"],
visibility = ["//visibility:private"],
)

genrule(
name = "generate_version_linkstamp",
outs = ["manual_linkstamp.cc"],
Expand Down Expand Up @@ -60,6 +69,20 @@ envoy_cc_library(
],
)

envoy_cc_library(
name = "api_version_includes",
hdrs = [
"api_version_struct.h",
":generate_api_version_number",
],
)

envoy_cc_library(
name = "api_version_lib",
hdrs = ["api_version.h"],
deps = [":api_version_includes"],
)

envoy_basic_cc_library(
name = "manual_version_linkstamp",
srcs = [":generate_version_linkstamp"],
Expand Down
23 changes: 23 additions & 0 deletions source/common/version/api_version.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include "common/version/api_version_struct.h"

// Defines the ApiVersion current version (Envoy::api_version), and oldest
// version (Envoy::oldest_api_version).
#include "common/version/api_version_number.h"

namespace Envoy {

/**
* Wraps compiled in api versioning.
*/
class ApiVersionInfo {
public:
// Returns the most recent API version that is supported by the client.
static constexpr ApiVersion apiVersion() { return api_version; }

// Returns the oldest API version that is supported by the client.
static constexpr ApiVersion oldestApiVersion() { return oldest_api_version; }
};

} // namespace Envoy
15 changes: 15 additions & 0 deletions source/common/version/api_version_struct.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>

namespace Envoy {

/**
* Api Version is defined by a <major>.<minor>.<patch> versions.
*/
struct ApiVersion {
uint32_t major;
uint32_t minor;
uint32_t patch;
};

} // namespace Envoy
6 changes: 6 additions & 0 deletions test/common/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ envoy_cc_test(
],
)

envoy_cc_test(
name = "api_version_test",
srcs = ["api_version_test.cc"],
deps = ["//source/common/version:api_version_lib"],
)

envoy_cc_test(
name = "statusor_test",
srcs = ["statusor_test.cc"],
Expand Down
19 changes: 19 additions & 0 deletions test/common/common/api_version_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "common/version/api_version.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace Envoy {

// Verify assumptions about oldest version vs latest version.
TEST(ApiVersionTest, OldestLatestVersionsAssumptions) {
constexpr auto latest_version = ApiVersionInfo::apiVersion();
constexpr auto oldest_version = ApiVersionInfo::oldestApiVersion();
// Same major number, minor number difference is at most 1, and the oldest patch is 0.
EXPECT_EQ(latest_version.major, oldest_version.major);
EXPECT_TRUE(latest_version.minor >= oldest_version.minor &&
latest_version.minor - oldest_version.minor <= 1);
EXPECT_EQ(0, oldest_version.patch);
}

} // namespace Envoy
29 changes: 29 additions & 0 deletions tools/api_versioning/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("@rules_python//python:defs.bzl", "py_binary", "py_test")
load(
"//bazel:envoy_build_system.bzl",
"envoy_package",
)

licenses(["notice"]) # Apache 2

envoy_package()

py_binary(
name = "generate_api_version_header_bin",
srcs = ["generate_api_version_header.py"],
main = "generate_api_version_header.py",
python_version = "PY3",
srcs_version = "PY3",
visibility = ["//visibility:public"],
)

py_test(
name = "generate_api_version_header_test",
srcs = ["generate_api_version_header_test.py"],
python_version = "PY3",
srcs_version = "PY3",
visibility = ["//visibility:public"],
deps = [
":generate_api_version_header_bin",
],
)
73 changes: 73 additions & 0 deletions tools/api_versioning/generate_api_version_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/python
"""Parses a file containing the API version (X.Y.Z format), and outputs (to
stdout) a C++ header file with the ApiVersion value.
"""
from collections import namedtuple
import pathlib
import string
import sys

ApiVersion = namedtuple('ApiVersion', ['major', 'minor', 'patch'])

FILE_TEMPLATE = string.Template("""#pragma once
#include "common/version/api_version_struct.h"
namespace Envoy {
constexpr ApiVersion api_version = {$major, $minor, $patch};
constexpr ApiVersion oldest_api_version = {$oldest_major, $oldest_minor, $oldest_patch};
} // namespace Envoy""")


def GenerateHeaderFile(input_path):
"""Generates a c++ header file containing the api_version variable with the
correct value.
Args:
input_path: the file containing the API version (API_VERSION).
Returns:
the header file contents.
"""
lines = pathlib.Path(input_path).read_text().splitlines()
assert (len(lines) == 1)

# Mapping each field to int verifies it is a valid version
version = ApiVersion(*map(int, lines[0].split('.')))
oldest_version = ComputeOldestApiVersion(version)

header_file_contents = FILE_TEMPLATE.substitute({
'major': version.major,
'minor': version.minor,
'patch': version.patch,
'oldest_major': oldest_version.major,
'oldest_minor': oldest_version.minor,
'oldest_patch': oldest_version.patch
})
return header_file_contents


def ComputeOldestApiVersion(current_version: ApiVersion):
"""Computest the oldest API version the client supports. According to the
specification (see: api/API_VERSIONING.md), Envoy supports up to 2 most
recent minor versions. Therefore if the latest API version "X.Y.Z", Envoy's
oldest API version is "X.Y-1.0". Note that the major number is always the
same as the latest version, and the patch number is always 0. In addition,
the minor number is at least 0, and the oldest api version cannot be set
to a previous major number.
Args:
current_version: the current API version.
Returns:
the oldest supported API version.
"""
return ApiVersion(current_version.major, max(current_version.minor - 1, 0), 0)


if __name__ == '__main__':
input_path = sys.argv[1]
output = GenerateHeaderFile(input_path)
# Print output to stdout
print(output)
91 changes: 91 additions & 0 deletions tools/api_versioning/generate_api_version_header_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Tests the api version header file generation.
"""
import generate_api_version_header
from generate_api_version_header import ApiVersion
import os
import pathlib
import string
import tempfile
import unittest


class GenerateApiVersionHeaderTest(unittest.TestCase):
EXPECTED_TEMPLATE = string.Template("""#pragma once
#include "common/version/api_version_struct.h"
namespace Envoy {
constexpr ApiVersion api_version = {$major, $minor, $patch};
constexpr ApiVersion oldest_api_version = {$oldest_major, $oldest_minor, $oldest_patch};
} // namespace Envoy""")

def setUp(self):
# Using mkstemp instead of NamedTemporaryFile because in windows NT or later
# the created NamedTemporaryFile cannot be reopened again (see comment in:
# https://docs.python.org/3.9/library/tempfile.html#tempfile.NamedTemporaryFile)
self._temp_fd, self._temp_fname = tempfile.mkstemp(text=True)

def tearDown(self):
# Close and delete the temp file.
os.close(self._temp_fd)
pathlib.Path(self._temp_fname).unlink()

# General success pattern when valid file contents is detected.
def SuccessfulTestTemplate(self, output_string, current_version: ApiVersion,
oldest_version: ApiVersion):
pathlib.Path(self._temp_fname).write_text(output_string)

# Read the string from the file, and parse the version.
output = generate_api_version_header.GenerateHeaderFile(self._temp_fname)
expected_output = GenerateApiVersionHeaderTest.EXPECTED_TEMPLATE.substitute({
'major': current_version.major,
'minor': current_version.minor,
'patch': current_version.patch,
'oldest_major': oldest_version.major,
'oldest_minor': oldest_version.minor,
'oldest_patch': oldest_version.patch
})
self.assertEqual(expected_output, output)

# General failure pattern when invalid file contents is detected.
def FailedTestTemplate(self, output_string, assertion_error_type):
pathlib.Path(self._temp_fname).write_text(output_string)

# Read the string from the file, and expect version parsing to fail.
with self.assertRaises(assertion_error_type,
msg='The call to GenerateHeaderFile should have thrown an exception'):
generate_api_version_header.GenerateHeaderFile(self._temp_fname)

def test_valid_version(self):
self.SuccessfulTestTemplate('1.2.3', ApiVersion(1, 2, 3), ApiVersion(1, 1, 0))

def test_valid_version_newline(self):
self.SuccessfulTestTemplate('3.2.1\n', ApiVersion(3, 2, 1), ApiVersion(3, 1, 0))

def test_invalid_version_string(self):
self.FailedTestTemplate('1.2.abc3', ValueError)

def test_invalid_version_partial(self):
self.FailedTestTemplate('1.2.', ValueError)

def test_empty_file(self):
# Not writing anything to the file
self.FailedTestTemplate('', AssertionError)

def test_invalid_multiple_lines(self):
self.FailedTestTemplate('1.2.3\n1.2.3', AssertionError)

def test_valid_oldest_api_version(self):
expected_latest_oldest_pairs = [(ApiVersion(3, 2, 2), ApiVersion(3, 1, 0)),
(ApiVersion(4, 5, 30), ApiVersion(4, 4, 0)),
(ApiVersion(1, 1, 5), ApiVersion(1, 0, 0)),
(ApiVersion(2, 0, 3), ApiVersion(2, 0, 0))]

for latest_version, expected_oldest_version in expected_latest_oldest_pairs:
self.assertEqual(expected_oldest_version,
generate_api_version_header.ComputeOldestApiVersion(latest_version))


if __name__ == '__main__':
unittest.main()

0 comments on commit 1d2c26f

Please sign in to comment.